2. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、自定义 error 类型

Go 语言没有传统面向对象语言中的异常(Exception)机制,而是采用显式错误返回的设计哲学。函数调用者必须主动检查并处理错误,这种设计让错误处理路径清晰可见,避免了隐式异常传播带来的不确定性。本章将系统讲解 Go 错误处理的核心基础:error 接口、标准错误构造方式以及自定义错误类型。

2.1 error 接口:一切错误的源头

Go 语言内置的 error 是一个预声明接口,定义在 builtin 包中。任何实现了该接口的类型都可以作为错误值传递。

// error 接口定义(builtin 包)
type error interface {
    Error() string
}

该接口只包含一个方法 Error() string,返回错误的描述信息。这意味着:

  • 任何类型只要实现了 Error() string 方法,就自动满足 error 接口。
  • 函数通过返回 error 类型来传递错误,调用方通过 if err != nil 判断。
  • nil 表示没有错误,非 nil 表示发生了错误。
核心原则:在 Go 中,错误就是实现了 Error() string 方法的任何值。错误处理是值处理,不是控制流处理。

2.2 快速构造错误:errors.New 与 fmt.Errorf

标准库提供了两种最常用的错误构造方式,适用于大多数简单场景。

构造方式 函数签名 特点 适用场景
errors.New func New(text string) error 返回一个包含固定文本的 error 值 静态错误消息,无需格式化
fmt.Errorf func Errorf(format string, a ...any) error 支持 fmt.Sprintf 风格的格式化 需要动态拼接错误信息
// errors.New 示例
var ErrNotFound = errors.New("resource not found")

func FindUser(id int) (User, error) {
    if id <= 0 {
        return User{}, ErrNotFound
    }
    // ...
}

// fmt.Errorf 示例
func OpenConfig(path string) (*Config, error) {
    if path == "" {
        return nil, fmt.Errorf("invalid config path: %s", path)
    }
    // ...
}
最佳实践:对于包内可复用的错误常量,建议使用 var ErrXxx = errors.New("...") 声明为包级变量,方便调用方使用 == 比较。对于需要携带动态信息的错误,优先使用 fmt.Errorf。

2.3 自定义 error 类型:携带更多上下文

当错误需要包含结构化信息(如错误码、状态码、内部原因)时,自定义 error 类型是更好的选择。只需定义一个结构体并实现 Error() string 方法。

// 自定义错误类型:携带错误码和内部原因
type AppError struct {
    Code    int    // 业务错误码
    Message string // 用户可读的消息
    Err     error  // 内部包装的原始错误(可选)
}

// 实现 error 接口
func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("code=%d, message=%s, cause=%v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}

// 使用示例
func ProcessOrder(id string) error {
    if id == "" {
        return &AppError{
            Code:    4001,
            Message: "order id is empty",
            Err:     nil,
        }
    }
    // ...
    return nil
}

自定义错误类型的优势:

  • 类型断言:调用方可以通过 err.(*AppError) 获取结构化字段。
  • 错误码匹配:直接比较 Code 字段,避免字符串解析。
  • 错误链支持:可以嵌套 Err 字段,配合 errors.Unwrap 实现错误链。

2.4 三种错误构造方式对比

维度 errors.New fmt.Errorf 自定义 error 类型
构造复杂度 最低 低 中
携带结构化数据 否 仅字符串 是(任意字段)
类型断言支持 否 否 是
错误链支持 否 Go 1.13+ 支持 %w 需手动实现 Unwrap
适用场景 简单静态错误 动态消息错误 业务错误码、多字段错误

2.5 错误处理基础流程(SVG 图)

下图展示了 Go 错误处理的核心流程:函数返回 error,调用方检查并决策。

Go 错误处理基础流程 Go 错误处理基础流程 函数调用 返回 (result, error) err != nil ? 是 处理错误 / 返回 否 正常使用 result 核心原则:错误是值,必须显式检查;非 nil 即处理,nil 即继续

2.6 错误类型体系结构(SVG 图)

下图展示了 Go 错误类型的层次关系:从接口到标准实现,再到自定义扩展。

Go 错误类型体系结构 Go 错误类型体系结构 error 接口 Error() string 标准库实现 errors.New(text) fmt.Errorf(format, ...) 自定义 error 类型 struct + Error() string 方法 所有错误类型都隐式实现 error 接口,通过 Error() 返回描述字符串

2.7 常见陷阱与最佳实践

常见陷阱:
  • 错误值比较陷阱:使用 == 比较 errors.New 创建的变量时,必须确保是同一个变量引用,而不是相同文本的不同实例。
  • 忽略错误:使用 _ 丢弃错误返回值会导致潜在问题,除非明确知道该错误可忽略。
  • 错误字符串解析:不要通过字符串匹配(如 strings.Contains)来判断错误类型,应使用类型断言或错误码。
最佳实践清单:
  1. 函数最后一个返回值应为 error 类型。
  2. 成功时返回 nil,失败时返回非 nil 错误。
  3. 对于可复用的静态错误,声明为包级变量(var ErrXxx = errors.New(...))。
  4. 需要携带动态信息时,优先使用 fmt.Errorf。
  5. 需要结构化错误信息(如错误码)时,定义自定义 error 类型。
  6. 始终检查错误:if err != nil { ... }。

2.8 小结

本章覆盖了 Go 错误处理的基础知识:

  • error 接口是 Go 错误处理的核心抽象,任何实现了 Error() string 的类型都是错误。
  • errors.New 用于构造静态错误消息,fmt.Errorf 用于格式化动态消息。
  • 自定义 error 类型通过结构体 + 方法实现,可以携带错误码、内部原因等结构化数据。
  • 三种构造方式各有适用场景,选择依据是错误信息的复杂度和结构化需求。