必须用 %w:仅当需上层通过 errors.is 或 errors.as 识别原错误时;%v 或字符串拼接会丢失类型和链式能力,导致判断失效、panic 或无法解包。

fmt.Errorf 用 %w 还是 %v?关键看上层要不要识别原错误
必须用 %w,而不是 %v 或字符串拼接,当且仅当你希望上层代码能用 errors.Is 或 errors.As 判断或提取底层错误时。
-
fmt.Errorf("read %s: %v", path, err)→ 原错误被转成字符串,类型丢失:errors.Is(err, fs.ErrNotExist)永远返回false -
"failed: " + err.Error()→ 更危险:如果err是nil,err.Error()会 panic,导致二次崩溃 -
fmt.Errorf("read %s: %w", path, err)→ 正确:保留对原错误的引用,自动实现Unwrap(),链可递归遍历
errors.Is 和 errors.As 为什么不能用 == 替代
== 只比较最外层错误值,而真实错误往往被多层包装。例如:
err := fmt.Errorf("http handler: %w", fmt.Errorf("db query: %w", os.ErrNotExist))
此时 err == os.ErrNotExist 是 false,但 errors.Is(err, os.ErrNotExist) 会逐层 Unwrap() 直到匹配或链结束。
-
errors.Is还兼容实现了Is(error) bool方法的自定义错误(如某些数据库驱动) -
errors.As尝试将链中**任意一层**的错误赋值给目标变量,不要求是最内层,也不需要你手动解包多次 - 单元测试里必须用
errors.Is(err, wantErr),否则加了包装后测试必然失败
自定义错误类型如何真正参与错误链
默认情况下,自定义结构体错误不会被 errors.Is 或 errors.As 识别——它不自动进入链式遍历。
- 必须显式实现
Unwrap() error方法,返回内部持有的底层错误(如func (e *MyError) Unwrap() error { return e.cause }) - 如果不包装其他错误(纯业务码),
Unwrap()应返回nil,而非自身或未初始化字段,否则errors.Unwrap可能死循环或 panic - 上下文字段(如
traceID、重试次数)放在结构体里供日志直接读取,Unwrap()仍只返回cause,不混入链判断逻辑
pkg/errors 能补上标准库缺的堆栈,但要注意使用边界
标准库 fmt.Errorf + %w 支持链式,但不记录堆栈;github.com/pkg/errors 的 New 和 Wrap 能自动捕获调用点,配合 %+v 输出完整堆栈。
- 开发阶段推荐用
pkg/errors.Wrap(err, "context")或pkg/errors.New("message"),再用fmt.Printf("%+v", err)查堆栈 - 生产环境建议统一接入日志库(如
zap),把%+v输出作为字段写入结构化日志,避免运行时开销过大 - 别在热路径循环里反复调用
pkg/errors.Wrap—— 堆栈捕获有性能成本,且每层都存一份调用帧,内存占用明显上升
真正容易被忽略的是:错误链不是越深越好。过度包装会让 errors.Is 查找变慢,也掩盖关键上下文。包装只应在语义上有新含义时发生(比如“打开配置文件失败”,而不是“调用了 Open”)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











