fmt.errorf("%w")是go 1.13+保留错误链的唯一标准方式;用%v、%s或字符串拼接会返回无unwrap()方法的*fmt.errorstring,导致errors.is和errors.as失效。

fmt.Errorf("%w") 是唯一能保留错误链的包装方式
不用 %w,就等于主动切断错误链。用 %v、%s 或字符串拼接(比如 "failed: " + err.Error())都会把原始错误转成字符串,返回的错误类型是 *fmt.errorString,它没有 Unwrap() 方法,errors.Is() 和 errors.As() 一查就失效。
常见现象:errors.Is(err, io.EOF) 总是 false,哪怕最内层就是 io.EOF——八成是某一层用了 %v。
-
%w后面必须是实现了error接口的非nil值;传string、nil或没实现Error()的结构体会直接 panic 或编译失败 - 一个
fmt.Errorf调用里只能有一个%w;写两个会 panic - 想加多层上下文,得链式调用:
fmt.Errorf("L2: %w", fmt.Errorf("L1: %w", origErr))
包装前必须检查 err != nil
fmt.Errorf("read header: %w", err) 在 err == nil 时会 panic,不是返回 nil。这是线上服务静默崩溃的高频原因。
正确做法永远是先判空:
- 如果业务逻辑允许返回
nil,就显式写if err != nil { return fmt.Errorf("xxx: %w", err) },然后单独return nil - 不要依赖 “上层会处理 nil” —— 包装函数自己就得守好这道门
- 在 defer 或中间件里做统一包装时,更要加
if err != nil判断,否则 panic 会吞掉原始错误栈
加 trace_id、user_id 等字段不能塞进 %w 格式串
%w 只负责嵌套错误,不负责拼上下文字段。把 trace_id 直接塞进 fmt.Errorf 的格式串里(比如 fmt.Errorf("trace_id=%s: %w", t, err))看似可行,但会导致两个问题:一是字段混在错误文本里,难解析;二是字段多了之后,错误消息变得臃肿,且无法被结构化日志工具提取。
推荐做法是封装辅助函数:
- 字段少时,直接写进外层描述:
fmt.Errorf("process user %s: %w", userID, err) - 字段多或需复用时,用 map 封装:
WrapWithCtx(err, "processing order", map[string]string{"trace_id": t, "user_id": u}),内部仍只用一次%w - 绝对不要在
defer里靠ctx.Value补字段——错误返回后ctx很可能已失效,且破坏错误自包含性
errors.Is 失效?先看 fmt.Printf("%+v", err)
fmt.Printf("%+v", err) 是最直接的诊断手段。它会打印整条错误链,包括每层包装的文件位置和行号(前提是没被第三方库覆盖)。如果输出里看不到原始错误(比如 io.EOF),说明链已在某层断裂。
此时重点排查:
- 是否某处用了
%v或%s替代%w - 是否某层包装传了
nil导致 panic 后 fallback 到其他错误路径 - 是否混用了
github.com/pkg/errors.Wrap—— 它的错误类型不实现标准Unwrap(),和errors.Is不兼容 - 自定义错误类型是否漏写了
Unwrap() error方法(返回nil也不行)
错误链越深,调试成本越高;生产环境建议控制包装深度 ≤ 4 层,避免日志爆炸或性能损耗。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











