标准库 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 在错误链中的遍历逻辑:

errors.Is / errors.As 错误链遍历流程 顶层错误 (err) 检查: Is/As 匹配? 返回 true 调用 Unwrap() Unwrap 返回 nil? 返回 false 继续检查下一层 Is 匹配值(哨兵错误),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 错误断言的标准范式。