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

如何在Golang中设计通用错误结构_Golang错误结构体设计方案  第1张

Go 语言原生错误(error)是接口,不是结构体,所以“通用错误结构体”本身是个伪命题——你无法让所有错误都统一成某个 struct 类型,但可以设计一个可扩展、可序列化、带上下文的错误包装方案。

fmt.Errorf + %w 包装错误时,为什么原始错误信息会丢失?

因为 fmt.Errorf("failed: %w", err) 只保留了底层错误的 Error() 方法返回值,不自动继承其自定义字段(如 code、traceID)。如果你依赖 errors.Iserrors.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 映射和日志脱敏。这些地方一旦松散,再好的结构体也形同虚设。