Go原生error是接口,需通过实现Unwrap()、Code()等方法构建可识别、可序列化的业务错误结构;嵌入error类型易导致Unwrap()不可控和errors.As匹配失败,应显式组合并实现必要方法。

Go 语言原生错误(error)是接口,不是结构体,所以“通用错误结构体”本身是个伪命题——你无法让所有错误都统一成某个 struct 类型,但可以设计一个可扩展、可序列化、带上下文的错误包装方案。
用 fmt.Errorf + %w 包装错误时,为什么原始错误信息会丢失?
因为 fmt.Errorf("failed: %w", err) 只保留了底层错误的 Error() 方法返回值,不自动继承其自定义字段(如 code、traceID)。如果你依赖 errors.Is 或 errors.As 判断类型,必须确保被包装的错误实现了 Unwrap() 方法。
- 使用
fmt.Errorf包装时,务必加%w(不是%s),否则链路断裂 - 若需透传业务码,不要只靠字符串拼接,而应让底层错误实现
Unwrap()并暴露Code()方法 -
errors.As(err, &target)能成功提取,前提是目标类型在错误链中某一层直接是该类型(或实现了As()方法)
如何定义可识别、可序列化的业务错误结构?
定义一个结构体实现 error 接口,并额外提供 Code()、Details() 等方法。关键点在于:它必须支持错误链(Unwrap()),且不破坏标准库的错误判断逻辑。
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
cause error
}
func (e *AppError) Error() string {
if e.cause != nil {
return fmt.Sprintf("%s: %v", e.Message, e.cause)
}
return e.Message
}
func (e *AppError) Unwrap() error { return e.cause }
func (e *AppError) Code() int { return e.Code }
func (e *AppError) As(target interface{}) bool {
if t, ok := target.(*AppError); ok {
*t = *e
return true
}
return false
}
- 不要把
cause设为私有字段后不实现Unwrap(),否则errors.Is无法穿透 - 如果要 JSON 序列化(比如日志或 API 响应),确保字段可导出且加
json:tag - 避免在
Error()中拼接敏感信息(如数据库密码),日志里只打e.Error()很危险
为什么不应该用嵌入方式实现通用错误?
像 type AppError struct{ *errors.Err } 这种嵌入看似省事,但会导致两个问题:一是 Unwrap() 行为不可控(嵌入类型可能没实现);二是 errors.As 无法精准匹配到你的结构体类型,因为 Go 的类型系统认的是具体类型,不是嵌入关系。
立即学习“go语言免费学习笔记(深入)”;
- 嵌入
error字段(如cause error)是安全的;嵌入一个已实现error接口的 struct 类型则容易失控 - 若想复用字段定义,可用组合+匿名字段,但必须显式实现
Error()和Unwrap() - 第三方库如
pkg/errors已停止维护,github.com/go-errors/errors不兼容 Go 1.13+ 错误链,建议手写轻量封装
真正难的不是定义结构体,而是统一错误构造入口、规范包装层级、并在 HTTP 中间件或 RPC 拦截器里做一致的 code 映射和日志脱敏。这些地方一旦松散,再好的结构体也形同虚设。










