Go 错误包装与链式错误

Go 1.13+ 引入的错误包装机制,通过 %w 动词和 errors.Is/As 函数实现错误链的创建、检查与提取。

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

Go 错误包装与链式错误

一句话定义

错误包装允许在返回错误时附加上下文信息,同时保留原始错误,使得调用方既能了解错误发生的上下文,也能精确判断底层错误类型。

核心机制 / 工作原理

Go 1.13 在 fmt.Errorf 中引入了 %w 动词,用于将已有错误”包装”进新错误中,形成错误链。被包装的错误通过 Unwrap() 方法可达。

// %w 包装错误,保留原始错误引用
err := fmt.Errorf("database query failed: %w", originalErr)

// %v 也会格式化错误,但不会建立包装链
err2 := fmt.Errorf("database query failed: %v", originalErr)
// err2.Unwrap() == nil,无法追溯到 originalErr

errors.Is 沿错误链逐层展开,检查链中是否存在与目标错误匹配的值:

if errors.Is(err, sql.ErrNoRows) {
    // 即使 err 是经过多层包装的,也能匹配到底层的 sql.ErrNoRows
}

errors.As 沿错误链查找第一个能赋值给目标类型的错误,并将其提取出来:

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Failed path:", pathErr.Path)
}

自定义错误类型实现 Unwrap:

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string {
    return "query error: " + e.Err.Error()
}

func (e *QueryError) Unwrap() error {
    return e.Err
}

Go 1.20 还引入了 errors.Join,可将多个错误合并为一个,每个错误都可通过 errors.Is 单独匹配:

err := errors.Join(err1, err2, err3)
errors.Is(err, err1) // true

最小例子 / 最小场景

var ErrNotFound = errors.New("not found")

func findUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("findUser(%d): %w", id, err)
    }
    if user == nil {
        return nil, fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
    }
    return user, nil
}

func handler() {
    _, err := findUser(42)
    if errors.Is(err, ErrNotFound) {
        // 精确判断:用户不存在
    }
}

findUser 返回的错误信息包含函数名和参数(调试友好),同时通过 %w 保留了底层哨兵错误,调用方可以精确判断错误原因。

为什么重要

  • 调试友好:错误链记录了每一层的上下文,形成完整的错误路径,而非只有最底层的原始错误。
  • 精确判断errors.Iserrors.As 让调用方能在不解析字符串的情况下做出逻辑决策。
  • 向后兼容:老代码返回的错误仍然可以被新代码的 errors.Is/errors.As 匹配。

边界与易混淆点

  • %w%v 的区别至关重要:%v 仅拼接字符串,不建立包装链,用 %v 包装后 errors.Is 无法穿透。
  • errors.Is 做的是值比较(类似 ==),对于实现了 Is(error) bool 方法的自定义错误,会调用该方法判断。
  • errors.As 要求目标必须是非 nil 指针,否则会 panic。
  • errors.Join (Go 1.20) 包装的是多个独立错误,与 %w 包装单个错误的语义不同。Join 产生的错误 Unwrap() 返回的是 []error 切片。
  • 过度包装会导致错误信息冗长,建议只在错误跨越架构边界时包装(如从 DAO 层到 Service 层)。
创建于 2026/6/25 更新于 2026/6/25