标准库 errors 包详解:errors.New、errors.Is、errors.As、errors.Unwrap 的原理与使用场景
在 Go 语言中,错误处理是程序健壮性的基石。标准库 errors 包提供了四个核心函数:errors.New、errors.Is、errors.As 和 errors.Unwrap。理解它们的原理与适用场景,是编写可维护、可断言错误代码的前提。
1. errors.New:创建哨兵错误
errors.New 是最基础的错误构造器,它接收一个字符串并返回一个包含该文本的 error 接口值。每次调用都会分配一个新的、唯一的错误实例,即使字符串内容相同,两个错误也不相等(指针不同)。
使用场景:定义包级别的“哨兵错误”(Sentinel Error),用于调用方通过
== 或 errors.Is 进行精确匹配。
// 包级别定义哨兵错误
var ErrNotFound = errors.New("resource not found")
var ErrPermissionDenied = errors.New("permission denied")
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrNotFound
}
// ...
}
| 特性 | 说明 |
|---|---|
| 唯一性 | 每次调用返回不同的指针,即使字符串相同 |
| 不可变 | 错误消息在创建后无法修改 |
| 比较方式 | 推荐使用 errors.Is,而非直接 == |
2. errors.Is:错误链的断言匹配
errors.Is 用于判断错误链中是否包含某个特定的目标错误。它会沿着 Unwrap 返回的错误链逐层检查,直到找到与目标相等的错误或链结束。这解决了多层包装后无法直接使用 == 比较的问题。
var ErrNotFound = errors.New("not found")
// 包装错误
func LoadConfig(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("load config: %w", err) // %w 包装
}
return nil
}
// 断言检查
err := LoadConfig("/nonexistent")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在,使用默认配置")
}
关键原理:
errors.Is 会递归调用 Unwrap() 方法,直到返回 nil 或找到匹配项。如果错误实现了 Is(target error) bool 方法,则优先使用该方法自定义匹配逻辑。
3. errors.As:错误链的类型断言
errors.As 用于在错误链中查找第一个可以赋值给目标类型的错误。它同样会遍历整个错误链,但匹配的是类型而非值。一旦找到匹配类型,就将该错误赋值给目标指针,并返回 true。
type NetworkError struct {
Code int
Message string
}
func (e *NetworkError) Error() string {
return fmt.Sprintf("network error %d: %s", e.Code, e.Message)
}
func (e *NetworkError) Unwrap() error {
return nil
}
func callAPI() error {
return &NetworkError{Code: 503, Message: "service unavailable"}
}
err := callAPI()
var netErr *NetworkError
if errors.As(err, &netErr) {
fmt.Printf("网络错误码: %d, 详情: %s\n", netErr.Code, netErr.Message)
}
| 函数 | 匹配依据 | 典型场景 |
|---|---|---|
errors.Is |
值相等(或自定义 Is 方法) |
检查是否是指定的哨兵错误 |
errors.As |
类型匹配(赋值兼容) | 提取错误中的结构化字段 |
4. errors.Unwrap:解包一层错误
errors.Unwrap 是错误链遍历的基础。它接收一个 error,如果该错误实现了 Unwrap() error 方法,则返回其包装的下一层错误;否则返回 nil。标准库的 fmt.Errorf 使用 %w 动词时,生成的错误会自动实现 Unwrap 方法。
// 手动实现 Unwrap
type WrappedError struct {
msg string
err error
}
func (e *WrappedError) Error() string {
return fmt.Sprintf("%s: %v", e.msg, e.err)
}
func (e *WrappedError) Unwrap() error {
return e.err
}
// 使用 fmt.Errorf 自动包装
err1 := errors.New("原始错误")
err2 := fmt.Errorf("上下文: %w", err1)
fmt.Println(errors.Unwrap(err2)) // 输出: 原始错误
fmt.Println(errors.Unwrap(err1)) // 输出: <nil>
注意:不要直接调用
errors.Unwrap 进行循环解包,应优先使用 errors.Is 和 errors.As,它们内部已正确处理链遍历和边界条件。
5. 错误链与断言流程
下图展示了 errors.Is 和 errors.As 在错误链中的遍历逻辑:
6. 实战对比:Is vs As 的选择
| 场景 | 推荐函数 | 示例 |
|---|---|---|
| 检查是否文件不存在 | errors.Is(err, os.ErrNotExist) |
判断底层错误是否为特定哨兵 |
| 提取 HTTP 状态码 | errors.As(err, &httpErr) |
获取自定义错误中的状态码字段 |
| 判断数据库连接超时 | errors.Is(err, context.DeadlineExceeded) |
标准库定义的超时哨兵 |
| 获取 SQL 错误码 | errors.As(err, &mysqlErr) |
提取驱动特定的错误结构 |
7. 最佳实践与避坑指南
- 始终使用
%w包装错误:使用fmt.Errorf("context: %w", err)而非%v,以保留错误链。 - 哨兵错误定义为
var而非const:Go 不支持常量错误,使用var ErrX = errors.New("...")。 - 避免在
Is和As中修改错误:这两个函数是只读检查,不应有副作用。 - 自定义错误类型需实现
Unwrap:如果自定义错误包装了其他错误,务必实现Unwrap() error方法。 - 不要滥用
errors.Unwrap:手动循环解包容易遗漏边界条件,交给Is/As处理更安全。
核心原则:用
errors.New 定义哨兵,用 fmt.Errorf + %w 包装上下文,用 errors.Is 检查值,用 errors.As 提取类型。这四者构成了 Go 错误断言的标准范式。