直接用errors.new或fmt.errorf不够用,因其返回无结构error,无法携带错误码、重试标志等字段,也无法可靠判断类型或提取上下文;必须用导出字段结构体实现error接口并提供unwrap()和is()方法。

为什么直接用 errors.New 或 fmt.Errorf 不够用
因为它们返回的是无结构的 error 接口值,无法携带额外字段(如错误码、重试标志、原始 HTTP 状态),也难以判断错误类型或提取上下文。比如你收到一个 "failed to parse JSON" 错误,但不知道它来自哪个服务、是否可重试、要不要记录敏感日志——这些信息必须靠自定义类型承载。
常见错误现象:用 strings.Contains(err.Error(), "timeout") 做判断,一旦错误消息微调就失效;或层层 fmt.Errorf("wrap: %w", err) 后,丢失关键元数据。
- 自定义 error 类型应实现
error接口,并支持Unwrap()(若需包装) - 字段命名建议小写(如
code、retryable),避免暴露内部细节 - 不要在
Error()方法里拼接敏感数据(如 token、密码),而应通过专用方法暴露
如何定义带字段和错误码的自定义 error
最简方式是定义一个结构体,内嵌 error 字段用于包装底层错误,再添加业务字段:
type AppError struct {
Code int
Message string
Retryable bool
Err error // 可选,用于包装底层 error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
func (e *AppError) Is(target error) bool {
if t, ok := target.(*AppError); ok {
return e.Code == t.Code
}
return false
}
使用时注意:Code 字段不能靠 Error() 返回值判断,必须用 errors.As 提取指针;Is() 方法让 errors.Is 能按码匹配,比字符串比较更可靠。
- 如果不需要包装能力,可去掉
Err字段和Unwrap() - 若多个 error 类型共用相同字段,可抽象出嵌入式基础结构(如
BaseError) - 避免在
Error()中调用e.Err.Error(),否则可能触发无限递归(尤其当e.Err也是同类型)
如何正确包装错误并保留原始上下文
Go 1.13+ 的 %w 动词和 errors.Unwrap 是标准包装机制,但仅适用于 fmt.Errorf 返回的 error。自定义类型若想参与标准包装链,必须实现 Unwrap() 方法并返回非 nil 值。
典型误用:在自定义 error 的 Error() 里手动拼接底层错误消息(如 return e.Message + ": " + e.Err.Error()),这会丢失可编程访问能力,且破坏 errors.Is/errors.As 的语义。
- 包装新错误时,优先用
fmt.Errorf("context: %w", originalErr),而非自定义类型 - 若必须用自定义类型包装,确保其
Unwrap()返回原始 error,且不覆盖Is()行为 - 调用方用
errors.As(err, &target)提取自定义类型,用errors.Is(err, target)判断是否为某类错误(如errors.Is(err, ErrNotFound))
为什么 errors.As 和 errors.Is 容易失败
根本原因在于类型断言和接口动态性:如果错误链中某层返回了非指针类型的 error(如值接收者方法的 Unwrap()),errors.As 就无法将目标变量地址写入;如果 Is() 方法没正确定义匹配逻辑,errors.Is 就会跳过该节点。
常见坑点:
- 自定义 error 的
Is()方法用了值接收者,导致无法修改传入的target指针 - 包装时用了
fmt.Errorf("msg: %v", err)(%v而非%w),彻底切断包装链 - 在
Unwrap()中返回了新构造的 error(如errors.New("wrapped")),丢失原始错误引用
调试技巧:用 fmt.Printf("%+v", err) 查看错误链结构,确认每层 Unwrap() 是否返回预期值;用 errors.Unwrap 手动展开几层验证。
真正难的不是定义类型,而是让每一层包装都保持可追溯、可判断、不丢元数据——这要求所有中间件、HTTP 客户端、数据库驱动都遵循同一套错误建模规则。











