2. 基础错误模式:函数返回 error、if err != nil 模式、多返回值中的错误处理

Go 语言在错误处理上采用了一种显式且简洁的设计哲学:错误就是值(Error is a value)。与许多语言使用异常(Exception)不同,Go 要求开发者将错误作为函数返回值的正常组成部分,并在调用处立即检查。这种模式虽然增加了少量代码量,但极大地提升了代码的可读性和控制力。

2.1 核心模式:函数返回 error

在 Go 中,error 是一个预定义的接口类型:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误值传递。函数通过将 error 作为最后一个返回值,向调用方报告执行结果。

特性 说明
返回值位置 error 总是作为多返回值中的最后一个
成功约定 函数执行成功时返回 nil
失败约定 函数执行失败时返回非 nil 的 error 值
零值语义 nil 是 error 接口的零值,表示无错误
关键原则: 永远不要忽略错误返回值。即使你认为某个函数“不可能失败”,也要显式处理或使用 _ 忽略(但需谨慎)。

2.2 if err != nil 模式

这是 Go 代码中最常见的控制流模式。调用函数后,立即检查返回的 error 是否为 nil:

result, err := doSomething()
if err != nil {
    // 处理错误:记录日志、返回错误、重试等
    log.Printf("doSomething 失败: %v", err)
    return err
}
// 继续处理 result

这种模式的优势在于:

  • 显式性:错误路径和正常路径在代码层面清晰分离
  • 局部性:错误处理紧邻错误产生点,便于阅读和维护
  • 可组合性:多个错误检查可以串联,形成清晰的执行流
常见陷阱: 不要使用 if err != nil { panic(err) } 来替代错误返回。在业务代码中,panic 只应用于真正的不可恢复异常(如数组越界),而非常规业务错误。

2.3 多返回值中的错误处理

Go 函数支持返回多个值,这使得错误可以与正常结果共存。典型的模式是:

func ReadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("读取配置文件失败: %w", err)
    }
    cfg, err := parseConfig(data)
    if err != nil {
        return nil, fmt.Errorf("解析配置文件失败: %w", err)
    }
    return cfg, nil
}

调用方可以同时接收结果和错误:

cfg, err := ReadConfig("/etc/app/config.yaml")
if err != nil {
    // 处理错误,此时 cfg 为 nil
    return err
}
// 此时 cfg 有效,可以安全使用
场景 返回值模式 示例
查询/读取 (*T, error) user, err := FindUser(id)
写入/操作 (error) err := SaveUser(user)
带状态的操作 (T, bool, error) val, ok, err := TryGet(key)

2.4 错误处理流程可视化

下图展示了 Go 中典型的错误处理流程,从函数调用到错误传播的完整路径:

Go 基础错误处理流程 调用方 函数执行 调用 err != nil? 是 记录日志 返回/包装错误 否 继续正常流程 使用返回值 错误向上层传播 逐层包装/处理 💡 核心要点 • 每个可能失败的函数都应返回 error 作为最后一个返回值 • 调用后立即检查 err != nil,形成“守卫模式” • 错误处理代码与正常业务逻辑在视觉上分离,提高可读性

2.5 实战中的错误处理模式

在业务代码中,if err != nil 模式会频繁出现。以下是几种常见的处理策略:

  1. 直接返回:当前函数无法处理,将错误原样返回给上层
  2. 包装后返回:使用 fmt.Errorf("上下文: %w", err) 添加上下文信息
  3. 降级处理:使用默认值替代,记录错误但不中断流程
  4. 重试:对于临时性错误,进行有限次数的重试
最佳实践: 在业务层中,尽量使用 fmt.Errorf 的 %w 动词包装错误,保留原始错误链。这样上层可以使用 errors.Is 和 errors.As 进行错误类型判断,实现精细化的错误处理。

2.6 多返回值错误处理的常见误区

误区 错误示例 正确做法
忽略错误 result, _ := doSomething() 显式检查 err,即使确定不会失败也要处理
错误时使用返回值 result, err := doSomething(); if err != nil { return result } 错误时返回零值,不要使用可能无效的结果
重复检查 if err != nil { return err } else { ... } 使用 if err != nil { return err } 提前返回,避免 else 嵌套
错误信息模糊 return errors.New("error") 提供有上下文的信息:fmt.Errorf("读取用户 %d 失败: %w", id, err)