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,调用方检查并决策。
2.6 错误类型体系结构(SVG 图)
下图展示了 Go 错误类型的层次关系:从接口到标准实现,再到自定义扩展。
2.7 常见陷阱与最佳实践
常见陷阱:
- 错误值比较陷阱:使用
==比较errors.New创建的变量时,必须确保是同一个变量引用,而不是相同文本的不同实例。 - 忽略错误:使用
_丢弃错误返回值会导致潜在问题,除非明确知道该错误可忽略。 - 错误字符串解析:不要通过字符串匹配(如
strings.Contains)来判断错误类型,应使用类型断言或错误码。
最佳实践清单:
- 函数最后一个返回值应为
error类型。 - 成功时返回
nil,失败时返回非nil错误。 - 对于可复用的静态错误,声明为包级变量(
var ErrXxx = errors.New(...))。 - 需要携带动态信息时,优先使用
fmt.Errorf。 - 需要结构化错误信息(如错误码)时,定义自定义
error类型。 - 始终检查错误:
if err != nil { ... }。
2.8 小结
本章覆盖了 Go 错误处理的基础知识:
- error 接口是 Go 错误处理的核心抽象,任何实现了
Error() string的类型都是错误。 - errors.New 用于构造静态错误消息,fmt.Errorf 用于格式化动态消息。
- 自定义 error 类型通过结构体 + 方法实现,可以携带错误码、内部原因等结构化数据。
- 三种构造方式各有适用场景,选择依据是错误信息的复杂度和结构化需求。