Go 错误处理详解

Go 错误处理详解

核心原则

Go 的错误处理哲学:错误是值,不是异常。没有 try/catch,而是通过返回值显式传递错误,强制调用方处理。


1. error 接口

go 复制代码
type error interface {
    Error() string
}

所有错误都实现这个接口。内置的 errors.New()fmt.Errorf() 是最常用的创建方式:

go 复制代码
err := errors.New("文件不存在")
err := fmt.Errorf("打开文件失败: %w", err)  // %w 包装错误,支持 Unwrap

2. 三种惯用模式

模式一:直接检查(最常见)

go 复制代码
f, err := os.Open("file.txt")
if err != nil {
    return fmt.Errorf("打开文件: %w", err)
}
defer f.Close()

模式二:哨兵错误(Sentinel Errors)

go 复制代码
var ErrNotFound = errors.New("未找到")

func Find(id int) (*Item, error) {
    // ...
    if notFound {
        return nil, ErrNotFound
    }
    return item, nil
}

// 调用方用 errors.Is 判断
item, err := Find(1)
if errors.Is(err, ErrNotFound) {
    // 专门处理"未找到"
}

⚠️ 用 errors.Is() 而非 ==,因为被包装后 == 会失效。

模式三:自定义错误类型

go 复制代码
type NotFoundError struct {
    ID int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("ID %d 未找到", e.ID)
}

// 调用方用 errors.As 提取具体类型
var nfe *NotFoundError
if errors.As(err, &nfe) {
    fmt.Printf("未找到的 ID: %d\n", nfe.ID)
}

3. 错误包装(Error Wrapping)--- Go 1.13+

go 复制代码
// fmt.Errorf + %w 创建包装链
return fmt.Errorf("数据库查询失败: %w", dbErr)

// errors.Is --- 沿包装链检查是否有某个错误
errors.Is(err, sql.ErrNoRows)  // true,即使被包装了

// errors.As --- 沿包装链提取某个类型的错误
var pqErr *pq.Error
errors.As(err, &pqErr)

包装 vs 拼接的区别:

go 复制代码
fmt.Errorf("查询失败: %w", err)  // 包装,errors.Is/As 可穿透
fmt.Errorf("查询失败: %v", err)  // 拼接,信息丢失,无法 Unwrap

4. panic / recover --- 仅用于真正的异常

使用场景极窄:程序逻辑上不可能恢复的错误(如数组越界、nil 指针、初始化失败)。

go 复制代码
// panic:程序无法继续
func init() {
    if config == nil {
        panic("配置不能为空")  // 合理:启动时致命错误
    }
}

// recover:捕获 panic(仅在 defer 中有效)
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("内部错误: %v", r)
        }
    }()
    return a / b, nil  // b=0 时 panic 被 recover 捕获
}

原则:包的公共 API 不应该 panic,让调用方决定如何处理。

5. 错误处理的最佳实践

做法 说明
✅ 尽早返回 if err != nil { return err },减少嵌套
✅ 添加上下文 每层包装时加上当前操作的信息
✅ 用 %w 包装 保留错误链,方便上层诊断
✅ 导出哨兵错误 var ErrXxx = errors.New(...)
❌ 不要忽略错误 _ = DoSomething() 几乎总是错的
❌ 不要用 panic 做流程控制 不同于 Java 的 checked exception
❌ 不要在循环里重复创建错误 var ErrXxx 在包级别声明

6. Go 1.13 vs 之前对比

复制代码
Go 1.12 及之前:  if err == ErrNotFound  // 包装后失效
Go 1.13+:       if errors.Is(err, ErrNotFound)  // 穿透包装链

这是 Go 错误处理最大的演进,核心就是 errors.Is / errors.As + %w 包装

7. 实战示例:分层错误处理

go 复制代码
// Repository 层
var ErrUserNotFound = errors.New("用户不存在")

func (r *UserRepo) GetByID(ctx context.Context, id int) (*User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT ... WHERE id=$1", id)
    var u User
    if err := row.Scan(&u.ID, &u.Name); err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("user repo get by id %d: %w", id, ErrUserNotFound)
        }
        return nil, fmt.Errorf("user repo get by id %d: %w", id, err)
    }
    return &u, nil
}

// Service 层 --- 用 errors.Is 判断业务错误
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
    u, err := s.repo.GetByID(ctx, id)
    if errors.Is(err, ErrUserNotFound) {
        return nil, ErrUserNotFound  // 透传业务错误
    }
    if err != nil {
        return nil, fmt.Errorf("user service get: %w", err)
    }
    return u, nil
}

// HTTP 层 --- 根据错误类型返回不同状态码
if errors.Is(err, ErrUserNotFound) {
    c.JSON(404, gin.H{"error": "用户不存在"})
    return
}

一句话总结:Go 的错误处理就是"错误是值 + 显式检查 + 包装传递上下文",简单直接,没有魔法。

相关推荐
z200509301 小时前
C++中位图和布隆过滤器的一些面试题
开发语言·c++
Bat U2 小时前
JavaEE|文件操作和IO
java·开发语言
脉动数据行情2 小时前
Python 实现融通金行情数据对接(实时推送 + K 线 + 产品列表)
开发语言·python
skywalk81632 小时前
Trae生成的中文编程语言关键字(如“定“、“函“、“印“等)需要和标识符之间用 空格 隔开,以确保正确识别
服务器·开发语言·编程
请你喝可乐2 小时前
AI Agent Skill 高阶使用指南:从入门到精通
后端
用户962377954482 小时前
代码审计 | Struts2 —— S2-016 OGNL 注入原理
后端
红色的小鳄鱼3 小时前
前端面试js手写
开发语言·前端·javascript
9号达人3 小时前
为什么你应该在 MQ 里用多个消费者,而不是一个
java·后端·架构
海盗12343 小时前
C#中的IEqualityComparer<T>使用
开发语言·c#