fmt.Errorf 用 %w 包装错误是为了保留可遍历的错误链,使 errors.Is/As 能穿透找到原始错误;须实现 Unwrap() 方法,禁用 %s 拼接和混用 pkg/errors,避免冗余包装。

fmt.Errorf 里用 %w 包装错误,不是为了“加文字”,而是为了保留原始错误链
Go 的错误包装本质是构建可遍历的错误链,fmt.Errorf("xxx: %w", err) 的 %w 动词会把 err 作为底层错误嵌入,让 errors.Is 和 errors.As 能穿透多层找到原始错误。如果只用 %s 或字符串拼接,原始错误类型和值就丢了。
- 常见错误现象:
errors.Is(err, io.EOF)返回false,即使最内层错误确实是io.EOF—— 因为用了%s拼接,断掉了错误链 - 正确写法必须用
%w,且只能出现一次(多个%w会 panic) - 如果要加多个上下文字段(比如请求 ID、用户 ID),得先组合成新错误再包装,而不是塞进
fmt.Errorf的格式串里
自定义错误类型想支持 %w,必须实现 Unwrap() 方法
只有实现了 Unwrap() error 方法的类型,才能被 %w 正确嵌入,并被 errors.Unwrap、errors.Is 等识别为可展开的包装错误。
- 没实现
Unwrap()的结构体,哪怕字段里存了err error,也不会被%w处理——它只是个普通值,不是“包装” - 典型写法:
func (e *MyError) Unwrap() error { return e.err },其中e.err是你保存的原始错误 - 注意:如果
Unwrap()返回nil,errors.Unwrap就会停止展开,所以别误返回nil当默认值
errors.Wrap(来自 github.com/pkg/errors)和 fmt.Errorf(%w) 不兼容
github.com/pkg/errors 的 Wrap 和 Go 标准库的 fmt.Errorf(...%w...) 使用的是两套错误链机制:pkg/errors 依赖自己的 Causer 接口和 StackTrace(),而标准库只认 Unwrap()。
- 混用会导致
errors.Is找不到原始错误,因为pkg/errors的错误不实现标准Unwrap() - Go 1.13+ 应该完全放弃
pkg/errors,改用标准%w+errors.Is/errors.As - 迁移时注意:旧代码里的
errors.Wrap(err, "msg")要改成fmt.Errorf("msg: %w", err),否则错误链断裂
ctx.Value 不能代替错误包装,但可以和错误一起传递上下文信息
有人试图用 ctx.Value 存 request ID,然后在 defer 里统一加到错误上——这不行。错误一旦返回,调用方很可能已经离开原 context,ctx.Value 不再可用;而且它破坏了错误的自包含性。
立即学习“go语言免费学习笔记(深入)”;
- 真正需要上下文丰富错误时,应该在出错点立刻构造带信息的新错误:
fmt.Errorf("failed to process user %s: %w", userID, err) - 如果字段太多(如 traceID、method、path),建议封装一个辅助函数:
WrapWithDetails(err, "processing", map[string]string{"trace_id": t, "user_id": u}),内部仍用%w - 别在中间件里“偷偷”给所有错误加 context——这会让错误来源模糊,调试时分不清是谁加的、加了几次
错误包装最易被忽略的一点:不是所有错误都该被包装。底层 I/O 错误(如 os.PathError)本身已含路径和操作名,再包一层“failed to open config file: %w”反而冗余;而业务逻辑错误(如 “user not found”)才需要明确包装来源和上下文。包装的粒度,取决于谁会处理这个错误、需要什么信息来决策。










