当错误需被上层判断类型、提取原因或恢复时必须用%w,仅日志提示用%s;%w保留Unwrap链支持errors.Is/As穿透,%s仅字符串拼接丢失上下文。
fmt.Errorf 的 %w 而不是 %s
当错误需要被上层代码判断类型、提取原始原因或做针对性恢复时,必须用 %w 包装;仅用于日志打印或用户提示的错误,用 %s 更安全。用 %w 会把原错误嵌入新错误的 Unwrap() 链中,而 %s 只是字符串拼接,丢失了错误上下文。
常见误用场景:
io.EOF 用 %s 包装后返回,导致调用方无法用 errors.Is(err, io.EOF) 判断fmt.Errorf("query failed: %s", err),掩盖了底层 *pq.Error 类型,失去结构化处理机会fmt.Errorf(... %w) 的嵌套限制与性能影响Go 不限制嵌套层数,但每层 %w 都会增加一次 Unwrap() 调用开销。实际项目中建议控制在 3 层以内——多数业务错误链是「业务逻辑 → 底层库 → 系统调用」三层结构。
需注意:
%w 包装(如中间件重复 wrap)会导致 errors.Is 和 errors.As 行为异常,可能匹配到错误的中间层fmt.Errorf("retry #%d: %w", n, err) 这类带状态信息的包装时,应确保上层只 unwrap 一次,避免状态覆盖原始错误语义fmt.Sprintf + err.Error() 更轻量errors.Is 和 errors.As 检查包装后的错误只有用 %w 包装的错误才能被 errors.Is 或 errors.As 向下穿透查找。关键点在于:被检查的目标错误必须是原始错误类型(如 os.PathError),而非包装后的 *fmt.wrapError。
if errors.Is(err, os.ErrNotExist) {
// ✅ 正确:err 是 fmt.Errorf("open config: %w", os.ErrNotExist)
}
if errors.As(err, &pathErr) {
// ✅ 正确:pathErr 是 *os.PathError 类型变量
log.Printf("failed on path: %s", pathErr.P
ath)
}
容易踩的坑:
%w 包装的错误调用 errors.Is 总是返回 false
errors.As 时传入指针类型不匹配(如传 *os.PathError 却想匹配 *os.SyscallError)会静默失败Unwrap() error,必须确保它返回非 nil 错误才能被继续穿透%w 包装链如果要让自定义错误能被 %w 接入并支持 errors.Is/As,必须实现 Unwrap() error 方法,并确保返回值是可继续 unwrap 的错误(或 nil)。不要在 Unwrap() 中返回新构造的错误,否则破坏链式结构。
type MyError struct {
Msg string
Code int
Err error // 原始错误,可为 nil
}
func (e *MyError) Error() string { return e.Msg }
func (e *MyError) Unwrap() error { return e.Err } // ✅ 直接返回字段,不 new
特别注意:
Err 字段,Unwrap() 必须返回 nil,否则 errors.Is 会 panicUnwrap() 都应只返回一个错误,避免返回切片或组合错误(那是 errors.Join 的职责)Unwrap() 中加日志或副作用——它可能被频繁调用%w,你就承诺了这个错误链会被下游消费,而不是仅仅被打印。