2. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、自定义错误类型
在 Go 语言中,错误处理是构建健壮 API 的基石。Go 采用显式错误返回机制,而非异常抛出。本章将深入剖析 Go 错误处理的核心:error 接口、标准库的构造方法以及自定义错误类型,为后续的搜索错误码断言与接口契约奠定基础。
2.1 error 接口:一切错误的源头
Go 语言的内建 error 接口是所有错误类型的统一抽象。它只定义了一个方法:Error() string,用于返回错误的描述信息。
// 内建 error 接口定义
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型,都自动满足 error 接口。这意味着你可以为任何自定义类型附加错误信息,使其成为合法的错误值。
error 是一个接口,而非具体类型。函数返回 error 时,实际返回的是实现了该接口的具体实例。判断错误是否为 nil 时,需要同时检查接口的动态类型和动态值。
2.2 errors.New:最简单的错误构造
errors.New 函数接收一个字符串,返回一个包含该字符串的 error 值。它适用于构造静态、无上下文的简单错误。
import "errors"
var ErrNotFound = errors.New("resource not found")
var ErrInvalidInput = errors.New("invalid input parameter")
func FindUser(id int) (User, error) {
if id <= 0 {
return User{}, ErrInvalidInput
}
// ... 查找逻辑
return User{}, ErrNotFound
}
errors.New 构造的错误,其 Error() 返回的字符串是固定的。调用者通常只能通过字符串比较(不推荐)或 == 运算符与预定义的错误变量进行比较。字符串比较脆弱,推荐使用预定义错误变量进行相等性判断。
2.3 fmt.Errorf:带格式化上下文的错误
当需要将动态值嵌入错误消息时,fmt.Errorf 是首选。它支持 fmt 包的格式化动词,能够生成包含具体参数的错误描述。
import "fmt"
func GetUserByID(id int) (User, error) {
if id <= 0 {
return User{}, fmt.Errorf("invalid user ID: %d", id)
}
// ... 数据库查询
if err != nil {
return User{}, fmt.Errorf("query user by id %d failed: %w", id, err)
}
return user, nil
}
从 Go 1.13 开始,fmt.Errorf 支持 %w 动词,用于创建包装错误(wrapped error)。包装错误保留了原始错误的引用,允许调用者通过 errors.Is 和 errors.As 进行错误链的遍历与类型断言。
| 构造方式 | 动态内容 | 错误包装 | 适用场景 |
|---|---|---|---|
errors.New |
不支持 | 不支持 | 静态、预定义的错误常量 |
fmt.Errorf (无 %w) |
支持 | 不支持 | 带动态参数的描述性错误 |
fmt.Errorf (含 %w) |
支持 | 支持 | 需要保留原始错误链的场景 |
2.4 自定义错误类型:携带结构化数据
当错误需要包含多个字段、错误码、上下文或支持特定行为时,自定义错误类型是更强大的选择。通过实现 error 接口,你可以定义自己的错误结构体。
// 自定义错误类型:包含错误码和内部错误
type AppError struct {
Code int
Message string
Err error // 内部原始错误
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("code=%d, message=%s, internal=%v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}
// 可选:实现 Unwrap 方法,支持错误链
func (e *AppError) Unwrap() error {
return e.Err
}
自定义错误类型的优势在于:
- 结构化数据: 可以携带错误码、HTTP 状态码、时间戳等字段。
- 类型断言: 调用者可以通过
errors.As将错误转换为具体类型,提取结构化信息。 - 行为扩展: 可以添加额外方法,如
Temporary() bool或Retryable() bool。 - 错误链支持: 通过实现
Unwrap() error方法,使自定义错误参与错误链遍历。
2.5 错误处理核心流程
下图展示了 Go 错误处理的核心流程,从错误构造到调用者处理:
2.6 三种错误构造方式对比
下表从多个维度对比了三种错误构造方式,帮助你在不同场景下做出选择:
| 特性 | errors.New | fmt.Errorf | 自定义类型 |
|---|---|---|---|
| 动态消息 | 不支持 | 支持 | 支持 |
| 错误包装 | 不支持 | 支持 (%w) |
通过 Unwrap() |
| 结构化数据 | 无 | 无 | 可包含任意字段 |
| 类型断言 | 仅 == 比较 |
仅 == 比较 |
errors.As / 类型断言 |
| 性能开销 | 低 | 中(格式化) | 中(结构体分配) |
| 适用场景 | 预定义错误常量 | 带参数的描述性错误 | 需要错误码、重试标志等 |
2.7 错误断言与接口契约的关联
在《Go 搜索错误码断言接口契约》课程中,错误处理基础是后续章节的基石。自定义错误类型携带的错误码,将成为搜索断言的核心依据。通过 errors.As 将通用 error 转换为携带错误码的具体类型,客户端可以精确判断错误性质并采取相应策略。
errors.Is 和 errors.As 的深入用法;第 4 章开始构建搜索错误码体系。掌握本章的自定义错误类型,是后续实现错误码断言接口契约的前提。