2、自定义错误类型:实现 error 接口、errors.New 与 fmt.Errorf、携带上下文的错误结构体

Go 语言的错误处理核心是 error 接口。任何实现了该接口的类型都可以作为错误值传递。本章将深入剖析如何自定义错误类型,对比标准库的两种快速构造方式,并重点讲解如何构建携带丰富上下文的错误结构体,为后续的错误码断言奠定基础。

2.1 error 接口与自定义类型

error 是 Go 内置的预声明接口,只包含一个方法:

type error interface {
    Error() string
}

任何类型只要实现了 Error() string 方法,就自动满足 error 接口。这意味着我们可以创建任意复杂度的错误类型,而不仅仅是字符串。

核心原则:自定义错误类型应始终实现 Error() string 方法,并通常使用指针接收者以避免复制时丢失类型信息。

下面是一个基础的自定义错误类型示例:

// MyError 自定义错误类型
type MyError struct {
    Code    int
    Message string
}

// Error 实现 error 接口
func (e *MyError) Error() string {
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}

// 使用示例
func doSomething() error {
    return &MyError{Code: 404, Message: "resource not found"}
}

2.2 errors.New 与 fmt.Errorf 的对比

标准库提供了两种快速创建简单错误的方式,但它们都不支持携带结构化字段。理解它们的差异有助于在合适的场景做出选择。

特性 errors.New fmt.Errorf
基本功能 创建仅包含静态文本的错误 创建包含格式化文本的错误
格式化支持 不支持 支持 %s、%d、%v 等占位符
动态参数 无 可嵌入变量值
类型断言 无法提取结构化字段 无法提取结构化字段
适用场景 固定错误消息,如哨兵错误 需要拼接运行时值的临时错误
// errors.New 示例:哨兵错误
var ErrNotFound = errors.New("item not found")

// fmt.Errorf 示例:动态错误
userID := 1001
err := fmt.Errorf("user %d not found in database", userID)
注意:使用 fmt.Errorf 时,如果后续需要通过错误码断言提取字段,必须使用自定义错误类型。仅靠格式化字符串无法恢复结构化数据。

2.3 携带上下文的错误结构体

在实际项目中,错误往往需要携带多个维度的上下文信息,例如错误码、用户消息、内部错误原因、请求追踪 ID 等。自定义结构体是实现这一目标的最佳方式。

下面是一个生产级别的错误结构体设计:

// AppError 应用级错误结构体
type AppError struct {
    Code       int    // 业务错误码
    Message    string // 面向用户的友好提示
    Err        error  // 内部原始错误(可嵌套)
    RequestID  string // 请求追踪标识
    Timestamp  time.Time // 错误发生时间
}

// Error 实现 error 接口
func (e *AppError) Error() string {
    return fmt.Sprintf("[%s] code=%d, message=%s, err=%v",
        e.Timestamp.Format(time.RFC3339), e.Code, e.Message, e.Err)
}

// Unwrap 支持 errors.Is 和 errors.As 链式查找
func (e *AppError) Unwrap() error {
    return e.Err
}

// 工厂函数:快速构造带上下文的错误
func NewAppError(code int, message string, err error) *AppError {
    return &AppError{
        Code:      code,
        Message:   message,
        Err:       err,
        RequestID: getRequestIDFromContext(), // 从 context 提取
        Timestamp: time.Now(),
    }
}
最佳实践:为自定义错误类型实现 Unwrap() error 方法,使其与 errors.Is 和 errors.As 兼容,便于在调用链中逐层解包和断言。

2.4 错误结构体设计模式对比

根据项目复杂度,可以选择不同的错误结构体设计模式:

模式 字段数量 可扩展性 适用项目
简单字段型 2-3 个 低 小型工具、脚本
标准上下文型 4-6 个 中 Web 服务、API 网关
全量追踪型 8+ 个 高 微服务、分布式系统

2.5 错误结构体与断言的关系

自定义错误结构体的核心价值在于:通过类型断言或 errors.As 提取结构化字段,实现精确的错误码匹配。这是后续章节“错误码断言”的基础。

// 断言示例
err := someFunction()
var appErr *AppError
if errors.As(err, &appErr) {
    // 成功提取 AppError 结构体
    switch appErr.Code {
    case 1001:
        // 处理特定业务错误码
    case 1002:
        // 处理另一种错误
    default:
        // 兜底处理
    }
}
关键结论:没有自定义错误结构体,就无法携带结构化错误码;没有结构化错误码,就无法实现精确的断言匹配。自定义错误类型是整个错误码断言体系的基石。
自定义错误类型与断言关系图 自定义错误类型与断言关系 errors.New fmt.Errorf 自定义错误结构体 error 接口 类型断言 errors.As 提取错误码 说明: 只有自定义错误结构体能携带结构化字段(如 Code、Message), 从而在断言阶段精确匹配业务错误码。errors.New 和 fmt.Errorf 无法做到这一点。

2.6 实战:构建带错误码的错误结构体

下面是一个完整的实战示例,展示如何定义、构造和使用带错误码的错误结构体:

// 定义业务错误码常量
const (
    ErrCodeSuccess       = 0
    ErrCodeInvalidInput  = 1001
    ErrCodeUnauthorized  = 1002
    ErrCodeNotFound      = 1003
    ErrCodeInternalError = 5000
)

// BizError 业务错误结构体
type BizError struct {
    Code    int    // 业务错误码
    Message string // 错误描述
    Detail  string // 详细调试信息
}

func (e *BizError) Error() string {
    return fmt.Sprintf("biz_error: code=%d, message=%s", e.Code, e.Message)
}

// 工厂函数
func NewBizError(code int, message string) *BizError {
    return &BizError{Code: code, Message: message}
}

// 使用示例
func GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, NewBizError(ErrCodeInvalidInput, "invalid user id")
    }
    // 模拟数据库查询
    user, found := db.FindUser(id)
    if !found {
        return nil, NewBizError(ErrCodeNotFound, "user not found")
    }
    return user, nil
}

// 调用方断言
user, err := GetUser(-1)
if err != nil {
    var bizErr *BizError
    if errors.As(err, &bizErr) {
        fmt.Printf("错误码: %d, 消息: %s\n", bizErr.Code, bizErr.Message)
        // 根据错误码做不同处理
    }
}
推荐做法:将错误码定义为有意义的常量,并在工厂函数中统一构造。这样既保证了错误码的一致性,也方便后续的断言匹配和文档生成。

2.7 本章小结

  • error 接口是 Go 错误处理的核心契约,任何实现了 Error() string 的类型都是错误。
  • errors.New 适用于静态哨兵错误,fmt.Errorf 适用于动态格式化错误,但两者都无法携带结构化字段。
  • 自定义错误结构体可以携带错误码、消息、内部错误、追踪 ID 等上下文信息,是实现精确断言的基础。
  • 为自定义错误类型实现 Unwrap 方法,使其与 errors.Is 和 errors.As 兼容,便于链式解包。
  • 通过 errors.As 进行类型断言,可以提取错误结构体中的字段,实现基于错误码的业务逻辑分支。