errors.unwrap 只解一层包装,不递归;需手动循环调用并判空防死循环,且自定义错误须正确定义 unwrap() 方法;%w 是唯一安全包装方式,否则断链。

errors.Unwrap 只解一层,不是递归展开工具
errors.Unwrap 的作用非常明确:它只返回被当前错误直接包装的那一个 error,不会跳过中间层,也不会自动递归到最底层。常见误解是调用一次就能拿到“根因”,但实际结果往往是另一层包装错误。
比如:
err := fmt.Errorf("service: %w", fmt.Errorf("db: %w", io.EOF))
fmt.Println(errors.Unwrap(err)) // 输出: db: io.EOF,不是 io.EOF
- 必须手动循环调用,且每次都要检查返回值是否为
nil - 某些自定义错误(如日志装饰器)可能让
Unwrap()返回自身,不加判断会陷入死循环 - 建议加深度限制(如最多 10 层),并始终判断
unwrapped == err防止环状引用
为什么 errors.Is 比手写 Unwrap 循环更可靠
errors.Is 内部确实也是循环调用 errors.Unwrap,但它额外支持实现了 Is(error) bool 方法的错误类型——比如某些数据库驱动会重载该方法做语义等价判断(如超时错误在不同驱动下表现不同但逻辑等价)。
这意味着:
- 仅靠
for err != nil { err = errors.Unwrap(err) }可能漏掉这类语义匹配 -
errors.Is(err, io.EOF)成功,不代表链里某层== io.EOF,而是逐层调用Is()或Unwrap()后匹配成功 - 如果某层错误既没实现
Unwrap()也没实现Is(),errors.Is就会在该层停止,不再往下查
自定义错误类型必须正确实现 Unwrap() 才能接入链
你写的结构体错误要参与标准错误链,不能只实现 Error() string。关键点有三个:
-
Unwrap() error必须返回真实持有的底层错误字段(如e.Cause),不能返回硬编码值或未初始化字段 - 如果确实没包装任何错误,
Unwrap()应返回nil,而不是返回自身或 panic -
Unwrap()函数体内禁止副作用:不能打日志、不能修改状态、不能触发网络请求——因为errors.Is和errors.As可能在任意时机静默调用它
%w 是包装前提,%v 或字符串拼接会直接断链
只有用 %w 才能让 fmt.Errorf 自动生成可展开的包装错误。其他方式都会让错误链在那一层彻底断裂:
-
fmt.Errorf("read %s: %v", path, err)→errors.Is(err, os.ErrNotExist)永远返回false -
"failed: " + err.Error()→ 如果err是nil,会 panic - 哪怕只是多包一层
errors.New(err.Error()),也会丢失原始类型和Unwrap()能力
真正容易被忽略的是:fmt.Errorf("xxx: %w", nil) 会 panic,所以包装前必须确保被包装错误非 nil。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











