go 1.13+ 中必须用 %w 包装错误才能支持 errors.is/as 的链式判断;若上层需识别、响应或提取底层错误类型或值,就必须用 %w,否则类型和堆栈丢失。

Go 1.13+ 的 fmt.Errorf 配合 %w 是错误链式追踪的唯一可靠起点;不用它,errors.Is 和 errors.As 就形同虚设。
什么时候必须用 %w,而不是 %v 或字符串拼接
核心判断只有一条:上层是否需要识别、响应或提取底层错误的类型或值。如果答案是“是”,就必须用 %w。
-
%v或%s会把原始error转成字符串再塞进新错误——类型信息丢失,errors.Is(err, fs.ErrNotExist)永远返回false - 手动字符串拼接(如
"failed: " + err.Error())更糟:既丢类型,又丢堆栈,还可能因err.Error()panic 导致二次崩溃 -
%w不是格式化占位符,而是“包装指令”:它让新错误持有对原错误的引用,并自动实现Unwrap() error方法 - 典型误用:
fmt.Errorf("read %s: %v", path, err)→ 包装成功但链断裂;正确写法:fmt.Errorf("read %s: %w", path, err)
errors.Is 和 errors.As 为什么不能用 == 替代
因为 == 只比较最外层错误值,而真实错误往往被多层包装。例如 fmt.Errorf("http handler: %w", fmt.Errorf("db query: %w", os.ErrNotExist)),此时 err == os.ErrNotExist 是 false。
-
errors.Is(err, target)会先做err == target,再逐层调用errors.Unwrap直到匹配或链结束;它还兼容实现了Is(error) bool方法的自定义错误(如某些驱动) -
errors.As(err, &target)同理,它尝试将链中**任意一层**的错误赋值给&target,只要类型匹配;不保证是最近一层,也不需要你手动Unwrap多次 - 测试时务必用
errors.Is(err, wantErr),否则单元测试在加了包装后必然失败 - 别在循环里反复
errors.Is同一个err和多个目标——它内部已递归,性能无额外开销
自定义错误类型如何真正融入标准链机制
自定义结构体错误默认不参与链式遍历。errors.Is 和 errors.As 对它完全“视而不见”,除非你显式实现 Unwrap() 方法。
- 必须返回一个
error类型值,通常是字段中保存的底层错误(如func (e *MyError) Unwrap() error { return e.cause }) - 如果错误本身不包装其他错误(比如纯业务码错误),
Unwrap()应返回nil,而非自身或空指针 - 避免返回自身(
return e)或未初始化字段,否则errors.Unwrap循环会死锁或 panic - 若需同时携带 traceID、重试次数等上下文,
Unwrap()仍只返回cause字段;其他字段供日志或中间件直接读取,不参与链式判断
errors.Unwrap 的真实用途和常见误用
errors.Unwrap 是单层解包函数,不是“全链展开工具”。它的设计意图很窄:获取当前错误的直接下一层包装错误。
- 适合场景:快速检查某错误是否「直接」包装了某个特定底层错误(比如
if errors.Unwrap(err) == os.ErrNotExist) - 不适合场景:想拿到最底层原因——应改用
errors.Is或errors.As,或手动循环调用(注意判空和深度限制) - 对没实现
Unwrap()的错误(如errors.New("x")),它返回nil,不 panic;但对nil输入也返回nil,所以裸调前无需额外判空 - 不要写
for err != nil { err = errors.Unwrap(err) }无限循环——某些恶意或有缺陷的自定义错误可能返回自身,导致死循环;生产代码应加层数上限(如 10 层)
链式错误的关键不在“记录多少层”,而在“每一层是否都明确表达了因果关系”。过度包装(比如每层都加 %w 却不提供有效上下文)和遗漏包装(该用 %w 时用了 %v)一样危险——前者让日志冗长难读,后者让错误彻底失联。











