必须用%w,否则错误链必然断裂:errors.Is和errors.As失效,因fmt.Errorf("msg: %v", err)无Unwrap方法;%w是运行时强校验,传nil或非error类型会panic;多层包装需每层都用%w且被包装对象支持Unwrap;HTTP响应等场景不应泄露原始错误链。

必须用 %w,否则错误链就断了——errors.Is 和 errors.As 会完全失效,不是“可能不工作”,是必然查不到原始错误。
为什么 fmt.Errorf("msg: %v", err) 不算包装
它只是把 err.Error() 转成字符串拼进去,返回的是一个 *fmt.errorString,底层没有 Unwrap() 方法。调用 errors.Unwrap(err) 得到 nil,整条链在此终止。
-
errors.Is(err, io.EOF)返回false,哪怕err就是io.EOF包装而来 -
fmt.Printf("%+v", err)只显示最外层文本,看不到嵌套结构 - 日志里看到的永远是“msg: EOF”,而不是可追溯的错误路径
%w 的硬性约束和常见 panic 场景
%w 不是语法糖,是运行时强校验:它只接受非 nil 的 error 类型值。传错就 panic,不是静默失败。
- 传
nil:fmt.Errorf("read: %w", err)中err == nil→ 直接 panic - 传
string:fmt.Errorf("read: %w", err.Error())→ 编译报错:cannot use ... as type error - 多个
%w(Go fmt.Errorf("a: %w, b: %w", e1, e2) → 编译失败 -
%w后紧挨字符:fmt.Errorf("err:%w!", err)→ 解析失败,编译报错
多层包装和自定义错误怎么写才不断链
错误链能穿透几层,取决于每一层是否都用 %w 且被包装对象本身支持 Unwrap()。任何一环用 errors.New、fmt.Errorf(无 %w)或漏掉 Unwrap() 实现,链就断了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确链式:
fmt.Errorf("level2: %w", fmt.Errorf("level1: %w", io.EOF))→errors.Is(err, io.EOF)为true - 错误中断:
fmt.Errorf("empty: %w", errors.New("empty"))→errors.New不支持Unwrap(),链在此截断 - 自定义类型必须显式实现:
func (e *MyErr) Unwrap() error { return e.cause },否则errors.As(err, &target)永远失败 - 避免在
Unwrap()里返回nil(除非你明确想终止展开),否则errors.Is会提前退出
什么时候不该用 %w
不是所有加文字的地方都要上 %w。它解决的是“下游需要判断/提取原始错误”的问题,不是“让日志看起来更全”。
- HTTP handler 返回给客户端的错误:应统一转成
fmt.Errorf("internal error"),避免泄露%w链中的内部细节 - 高频路径(如每秒万级请求的中间件):过度包装增加堆分配,
fmt.Errorf("req %s failed: %v", id, err)更轻量 - 仅用于打日志:直接
slog.Error("failed", "err", err),日志库会自动调err.Error(),无需包装 - 错误本身不含需透出的原始类型(比如纯业务校验失败),用
errors.New或fmt.Errorf("xxx")即可
最容易被忽略的是:%w 包装后,errors.Is 和 errors.As 能穿透,但 fmt.Printf("%v", err) 不能——调试时得用 %+v 才能看到完整链。别靠 err.Error() 字符串匹配做逻辑分支,那是反模式。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










