go 中通过实现 unwrap() error 方法并使用 fmt.errorf("%w", err) 构建错误链,使 errors.is 和 errors.as 可递归穿透查找;自定义错误需显式返回底层错误,未实现 unwrap 则终止遍历。

Go 里怎么让错误带上下级关系
Go 的 error 是接口,本身不支持嵌套或继承。想实现“父错误包含子错误”这种层级关系,得靠 Unwrap 方法手动构造链式结构——标准库的 fmt.Errorf 用 %w 动词就能自动加一层包装,底层就是返回一个实现了 Unwrap 的私有类型。
常见错误现象:直接用 fmt.Errorf("failed: %v", err) 会丢失原始错误,导致 errors.Is 或 errors.As 查不到源头;用 %w 才算真正“包裹”,不是拼字符串。
- 必须用
%w(不是%v)才能让错误可展开,否则errors.Unwrap返回nil - 每个
%w只能包一个错误,不能一次包多个;如需多层,得嵌套调用fmt.Errorf - 自定义错误类型如果想参与这个链路,必须自己实现
Unwrap() error方法,返回下一级错误
自定义错误类型如何正确实现 Unwrap
你写了一个结构体错误类型,比如 ValidationError,想让它既能带字段信息,又能被 errors.Is 向下穿透查到根因,就必须显式实现 Unwrap。不实现就只是个“叶子节点”,没法构成链。
使用场景:API 层捕获 DB 错误,包装成业务错误再抛出;中间件统一加 traceID;校验失败时保留原始参数错误。
立即学习“go语言免费学习笔记(深入)”;
-
Unwrap方法返回error,不是指针也不是接口断言,直接返回你持有的底层错误字段 - 如果当前错误没有下级(比如是原始错误),
Unwrap应该返回nil,别 panic 或返回自身 - 不要在
Unwrap里做日志、格式化或网络请求——它可能被频繁调用,必须轻量
type ValidationError struct {
Field string
Err error // 存原始错误
}
func (e *ValidationError) Error() string { return "validation failed" }
func (e *ValidationError) Unwrap() error { return e.Err }
errors.Is 和 errors.As 怎么穿透多层找错
errors.Is 不是简单比地址,而是递归调用 Unwrap,一层层往下问“你是不是这个目标错误?”,直到 nil 或匹配成功。所以只要链上任意一环满足条件,就返回 true。
性能影响:链太深(比如超过 20 层)会有轻微开销,但绝大多数业务场景完全不用顾虑;兼容性没问题,1.13+ 均支持。
-
errors.Is(err, io.EOF)能命中fmt.Errorf("read failed: %w", io.EOF)包裹后的结果 -
errors.As(err, &target)同样递归查找,找到第一个能转型成功的实例,赋值给target - 注意:如果自定义错误没实现
Unwrap,那它就是链的终点,Is/As不会继续往下钻
容易踩的坑:包装时机和错误重复暴露
最常犯的错是在 defer、recover 或日志里反复包装同一个错误,导致链无限变长,或者把已包装过的错误再用 %w 包一遍,造成冗余层级。
典型表现:errors.Is(err, someErr) 突然不生效了;打印错误时看到一串重复的 “failed: failed: failed: …”;用 errors.As 找不到预期类型。
- 只在**语义升级**时才包装:比如 DB 层错误 → 业务逻辑错误 → HTTP 响应错误,每层职责清晰
- 避免在日志函数里用
%w包装,日志只需%v输出;包装是控制流的一部分,不是输出格式的事 - 检查是否已经包装过:可以用
errors.Unwrap判空 + 类型判断,但更稳妥的是设计时约定“谁负责包装”,比如只在 handler 入口统一 wrap
层级不是越多越好,三层以内通常够用;关键是每层都提供新信息,而不是为了包装而包装。










