go 1.13 错误包装需用 fmt.Errorf 配合 %w 显式声明,否则 errors.is/as 失效;%w 仅接受单个 error 且须为最后参数,不可混用 %v;安全检查应优先用 errors.is/as 而非字符串匹配;自定义 unwrap() 适用于多错误聚合、附加上下文等场景。

Go 1.13 引入的 errors 包错误包装机制,不是“加个字符串前缀”那么简单——它要求你用 fmt.Errorf 配合 %w 动词显式声明包装关系,否则 errors.Is 和 errors.As 会失效。
为什么 fmt.Errorf("failed: %v", err) 不算包装?
这种写法只是把原始错误转成字符串再拼接,丢失了错误链结构。底层 err 不再可被 errors.Unwrap() 访问,errors.Is(err, io.EOF) 必然返回 false,即使原始错误确实是 io.EOF。
正确做法是:
err := doSomething() if err != nil { return fmt.Errorf("processing item %s failed: %w", itemID, err) // ✅ 使用 %w }
关键点:
立即学习“go语言免费学习笔记(深入)”;
-
%w只接受一个error类型参数,且必须是最后一个参数 - 不能混用
%w和%v处理同一个错误值(比如fmt.Errorf("%w: %v", err, err)是错的) - 如果要包装多个错误,需嵌套调用或用自定义错误类型实现
Unwrap() error
如何安全地检查和提取被包装的错误?
errors.Is 和 errors.As 会自动沿 Unwrap() 链向上遍历,但前提是每层都用了 %w 或实现了标准接口。常见误用:
- 用
strings.Contains(err.Error(), "timeout")替代errors.Is(err, context.DeadlineExceeded)—— 无法匹配包装后的错误 - 对未包装的错误调用
errors.As(err, &target)失败后不检查是否是原始类型,直接 panic - 在中间层错误中漏掉
%w,导致下游无法追溯到根本原因
推荐检查模式:
if errors.Is(err, os.ErrNotExist) { // 处理文件不存在 } else if errors.Is(err, context.DeadlineExceeded) { // 处理超时 } else if errors.As(err, &myCustomErr) { // 提取自定义错误字段 }
什么时候该自己实现 Unwrap() 而非依赖 %w?
当需要包装多个错误、携带额外上下文(如时间戳、请求 ID)、或控制展开逻辑时,%w 不够用。例如:
- 一个 rpc 调用失败,你想同时保留客户端错误、服务端返回的 status code 和 trace ID
- 数据库事务中多个操作出错,需聚合所有子错误而非只留最后一个
- 想限制展开深度(避免无限递归),或按条件跳过某些中间错误
简单自定义包装示例:
type WrappedError struct { Msg string Cause error TraceID string } func (e *WrappedError) Error() string { return fmt.Sprintf("[%s] %s", e.TraceID, e.Msg) } func (e *WrappedError) Unwrap() error { return e.Cause // ⚠️ 返回单个 error;若要多错误,需返回 []error 并配合自定义 Is/As
真正难的不是包装,而是团队里有人忘了加 %w,或者在日志里只打 err.Error() 而丢掉了整个错误链——这些地方一漏,排查时就只剩“某个地方错了”,没有上下文,也没有可编程判断的依据。