Go中error接口本身不支持错误链,必须用fmt.Errorf的%w动词才能保留原始错误;自定义错误需实现Unwrap()方法,errors.Is和errors.As才有效;避免字符串匹配,慎用%+v防止敏感信息泄露。

Go 中 error 接口本身不支持错误链,直接用 fmt.Errorf 带 %w 才能保留原始错误
Go 1.13 引入了错误包装(error wrapping),但很多人误以为只要调用 fmt.Errorf("xxx: %v", err) 就能构成可追溯的错误链。实际上,这种写法只是字符串拼接,原始 err 被丢弃,errors.Is 和 errors.As 全部失效。
正确做法是显式使用 %w 动词:
if err != nil {
return fmt.Errorf("failed to read config: %w", err) // ✅ 可被 errors.Unwrap()
}
-
%w必须是最后一个动词,且只接受一个error类型参数 - 多个错误要包装?只能逐层套:
fmt.Errorf("level2: %w", fmt.Errorf("level1: %w", origErr)) - 用
%v或%s替代%w→ 错误链断裂,调试时查不到根因
自定义错误类型必须实现 Unwrap() 方法才能参与标准错误链
如果想带上下文字段(如请求 ID、重试次数)又希望兼容 errors.Is/errors.As,不能只嵌入 error 字段,必须显式提供 Unwrap() 方法。
type MyError struct {
Msg string
Code int
Err error // 包装的底层错误
}
func (e *MyError) Error() string { return e.Msg }
func (e *MyError) Unwrap() error { return e.Err } // ✅ 关键:让 errors.Unwrap 能往下钻
- 缺少
Unwrap()→errors.Is(err, target)永远返回false,即使e.Err == target - 如果错误可能有多个包装层级,
Unwrap()只需返回直接包裹的那一个(单级),标准库会自动递归 - 不要在
Unwrap()里做逻辑判断或返回nil(除非明确表示“无包裹”),否则破坏链式行为
用 errors.Is 判断哨兵错误,别用 == 或 strings.Contains
很多人把自定义错误当普通值比较:if err == ErrNotFound,这仅在错误是同一个指针时成立;一旦被 fmt.Errorf("%w", ...) 包装过,就失效了。
立即学习“go语言免费学习笔记(深入)”;
-
errors.Is(err, ErrNotFound)会沿着Unwrap()链一直找,直到匹配或返回nil -
errors.As(err, &target)用于类型断言,比如提取带字段的自定义错误:var myErr *MyError; if errors.As(err, &myErr) { ... } - 用
strings.Contains(err.Error(), "not found")是反模式:依赖字符串内容,易被日志前缀/翻译破坏,且无法区分同名不同义错误
生产环境慎用 fmt.Errorf("%+v", err) 打印错误链
%+v 能展开整个错误链并显示栈帧,开发期排查很有用,但上线后可能暴露敏感路径、变量名甚至内存地址(取决于具体 error 实现)。
- 默认日志输出建议用
err.Error()—— 它只返回顶层消息,安全可控 - 需要链式详情?改用
fmt.Sprintf("%v%+v", err, err)分开处理:前者给用户/监控,后者进 debug 日志(且确保日志系统已脱敏) - 第三方库(如
github.com/pkg/errors)的Wrap也输出栈,但 Go 标准库不带栈,混用时注意errors.Is仍可用,但栈信息会丢失
错误链不是越深越好,关键在每一层是否添加了不可替代的上下文;过度包装会让 errors.Is 查找变慢,也增加 Unwrap() 循环风险。真正该被包装的,是那些调用方无法直接理解、但业务逻辑必须响应的错误。其他一律用 %w 透传。


















