当错误需被上层判断类型、提取原因或恢复时必须用%w,仅日志提示用%s;%w保留Unwrap链支持errors.Is/As穿透,%s仅字符串拼接丢失上下文。

什么时候该用 fmt.Errorf 的 %w 而不是 %s
当错误需要被上层代码判断类型、提取原始原因或做针对性恢复时,必须用 %w 包装;仅用于日志打印或用户提示的错误,用 %s 更安全。用 %w 会把原错误嵌入新错误的 Unwrap() 链中,而 %s 只是字符串拼接,丢失了错误上下文。
常见误用场景:
- 在 HTTP handler 中把
io.EOF用%s包装后返回,导致调用方无法用errors.Is(err, io.EOF)判断 - 数据库操作失败后用
fmt.Errorf("query failed: %s", err),掩盖了底层*pq.Error类型,失去结构化处理机会
fmt.Errorf(... %w) 的嵌套限制与性能影响
Go 不限制嵌套层数,但每层 %w 都会增加一次 Unwrap() 调用开销。实际项目中建议控制在 3 层以内——多数业务错误链是「业务逻辑 → 底层库 → 系统调用」三层结构。
需注意:
立即学习“go语言免费学习笔记(深入)”;
- 同一错误被多次
%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.Path)
}
容易踩的坑:
- 对非
%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会 panic - 多个嵌套自定义错误时,每个
Unwrap()都应只返回一个错误,避免返回切片或组合错误(那是errors.Join的职责) - 不要在
Unwrap()中加日志或副作用——它可能被频繁调用
%w,你就承诺了这个错误链会被下游消费,而不是仅仅被打印。










