Go 错误包装与链式错误
Go 1.13+ 引入的错误包装机制,通过 %w 动词和 errors.Is/As 函数实现错误链的创建、检查与提取。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 知识体系
- 前置概念: Go 错误处理, Go defer、panic 与 recover
- 并列概念: Go Context
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.Is和errors.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 层)。