使用 fmt.Errorf("%w") 包装 nil 错误会 panic,因 %w 要求右侧必须是非 nil error;混用 errors.Wrap 和 %w 会导致 errors.Is 失效;用 %v 或字符串拼接会断链;自定义错误须显式实现 Unwrap() 才能参与标准错误链。

fmt.Errorf("%w") 包装 nil 错误会 panic
直接写 fmt.Errorf("read failed: %w", err) 且 err 为 nil 时,程序立即崩溃,错误类似 panic: interface conversion: interface {} is nil, not error。这不是 bug,而是 Go 对 %w 的硬性语义约束:它要求右侧必须是非 nil 的 error 值。
常见于封装 I/O 或中间件逻辑时,比如调用 io.ReadFull 后未判空就统一走包装返回路径。
- 必须显式判断:
if err != nil { return fmt.Errorf("read header: %w", err) },否则不能直接包装 - 函数末尾若无错误,应明确返回
nil,而不是试图“统一返回包装结果” - 别用三元表达式伪装安全:
fmt.Errorf("x: %w", err) // err 可能为 nil → panic
混用 errors.Wrap 和 fmt.Errorf("%w") 会导致 errors.Is 失效
errors.Wrap(来自 github.com/pkg/errors)和 fmt.Errorf("%w") 底层结构不兼容:前者返回带堆栈的自定义 struct,后者返回标准库的 *fmt.wrapError。二者虽都实现 Unwrap(),但类型不同,跨包比较时 errors.Is() 可能返回 false,即使原始错误相同。
典型现象:用 fmt.Errorf("%w", err) 包装后,再用 pkg/errors.Cause() 提取不到原始错误;日志中 %+v 也只显示文本,不展开堆栈。
立即学习“go语言免费学习笔记(深入)”;
- 新项目一律用原生
%w+errors.Is/errors.As,不引入pkg/errors - 老项目若已大量使用
errors.Wrap,替换时需全局搜索所有Cause()、Stack()调用点,并改用errors.Unwrap或fmt.Printf("%+v", err) - 禁止在同一个错误链中混用两种包装方式,哪怕只有一处也会破坏整条链的可判断性
用 %v 或字符串拼接会彻底断链
fmt.Errorf("failed: %v", err) 或 "failed: " + err.Error() 看起来只是格式不同,实际是完全不同的行为:它们把原始错误转成字符串,返回的是不带 Unwrap() 方法的纯文本错误,errors.Is(err, io.EOF) 必然返回 false,errors.As(err, &pathErr) 也永远失败。
调试时容易被误导——err.Error() 显示正常,但程序逻辑判断全部失效。
- 只要想让下游做类型判断或错误提取,就必须用
%w,且只能用一次 - 不要在
Error()方法里手动拼接e.Err.Error(),可能引发无限递归 - 验证是否真连通:运行
errors.Unwrap(err),看是否返回预期下层错误;或用fmt.Printf("%+v", err)观察是否展开多层
自定义 error 类型必须显式实现 Unwrap 才能参与标准链
定义了结构体错误(如 type ValidationError struct { Msg string; Cause error })后,如果不实现 Unwrap() error 方法,它就无法被 errors.Is 或 errors.As 穿透,哪怕你在外层用 %w 包装它也没用。
这个方法不是可选的“增强功能”,而是参与标准错误链的准入门槛。
- 实现只需一行:
func (e *ValidationError) Unwrap() error { return e.Cause } -
Cause字段必须是error类型,不能是*ValidationError或其他 - 若
Cause可能为nil,Unwrap()应直接返回nil,否则errors.Is会提前终止遍历 - 别在
Unwrap()里加日志、panic 或网络调用——它可能被高频、无上下文地调用
%v、少写一个 Unwrap(),下游所有错误分类逻辑就全废了。


















