3. 自定义错误类型:实现 error 接口、errors.New 与 fmt.Errorf、携带额外上下文信息

Go 语言内置的 error 接口仅包含一个 Error() string 方法,这在实际业务中远远不够。本章将深入讲解如何通过自定义错误类型来携带结构化上下文信息,并结合 errors.New、fmt.Errorf 以及错误断言技术,构建可溯源、可分类的业务错误体系。

3.1 error 接口的本质与自定义类型

任何实现了 Error() string 方法的类型都可以作为错误使用。自定义错误类型的核心价值在于:将错误码、错误消息、堆栈信息、业务字段等结构化数据封装在同一个对象中,从而在调用链中通过类型断言或 errors.As 精确提取。

对比维度 内置 error(字符串) 自定义错误类型
携带信息 仅文本消息 错误码、字段、堆栈、元数据
断言方式 字符串匹配(脆弱) 类型断言 / errors.As(稳定)
可扩展性 低 高(可嵌入、可组合)
业务语义 弱 强(直接映射业务场景)
最佳实践: 在业务层始终使用自定义错误类型,避免直接返回 errors.New("xxx") 或 fmt.Errorf("xxx") 作为最终错误。这些函数仅用于快速构造临时错误或包装底层错误。

3.2 实现一个完整的业务错误类型

以下代码展示了一个携带错误码、HTTP 状态码、内部消息和用户消息的自定义错误结构体:

// BizError 业务错误类型
type BizError struct {
    Code       int    // 业务错误码,如 10001
    HTTPStatus int    // HTTP 状态码,如 400
    Message    string // 内部日志消息
    UserMsg    string // 展示给用户的友好消息
    Err        error  // 原始错误(可选,用于链式包装)
}

// Error 实现 error 接口
func (e *BizError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

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

// 预定义错误码常量
const (
    ErrCodeNotFound     = 10001
    ErrCodeUnauthorized = 10002
    ErrCodeValidation   = 10003
)

// 工厂函数:快速创建带上下文的业务错误
func NewNotFoundError(resource string, err error) *BizError {
    return &BizError{
        Code:       ErrCodeNotFound,
        HTTPStatus: 404,
        Message:    fmt.Sprintf("资源 %s 未找到", resource),
        UserMsg:    "请求的资源不存在",
        Err:        err,
    }
}
关键设计点:
  • Error() 方法返回的字符串用于日志记录,不应直接展示给用户。
  • Unwrap() 方法使自定义错误可以参与 errors.Is 和 errors.As 的链式查找。
  • 工厂函数封装了错误码和消息的创建逻辑,避免在业务代码中手动拼装。

3.3 errors.New 与 fmt.Errorf 的正确使用场景

虽然自定义类型是主力,但 errors.New 和 fmt.Errorf 在以下场景中仍然不可或缺:

  • 快速构造哨兵错误: 使用 errors.New("EOF") 定义包级别的可比较错误变量。
  • 包装底层错误: 使用 fmt.Errorf("读取配置失败: %w", err) 保留原始错误链。
  • 临时占位: 在开发阶段或测试代码中快速生成错误。

注意: 不要将 fmt.Errorf 的 %w 用于包装自定义错误类型——这会导致丢失结构化字段。应使用自定义类型的 Unwrap 机制。

3.4 携带额外上下文信息的三种模式

模式 实现方式 适用场景
结构体字段 在自定义类型中定义业务字段 错误码、用户 ID、请求 ID 等固定字段
错误链包装 通过 Unwrap 串联多个错误 跨层调用时保留完整调用栈
上下文接口 定义 Context() map[string]interface{} 方法 需要动态附加键值对(如链路追踪)
避坑指南: 不要在错误对象中存储指针类型的可变状态(如切片、map),这会导致并发安全问题。如果需要动态上下文,使用不可变快照或值拷贝。

3.5 错误断言实战:从错误中提取业务信息

当调用方收到一个 error 接口值时,需要通过断言获取自定义类型中的结构化数据:

func handleOrder(orderID string) error {
    // 模拟业务逻辑返回 BizError
    return NewNotFoundError("order", nil)
}

func main() {
    err := handleOrder("ORD-001")
    if err != nil {
        // 方式一:类型断言(推荐用于单类型)
        var bizErr *BizError
        if errors.As(err, &bizErr) {
            fmt.Printf("错误码: %d, 用户消息: %s\n", bizErr.Code, bizErr.UserMsg)
            // 根据错误码做不同处理
            switch bizErr.Code {
            case ErrCodeNotFound:
                // 触发 404 响应
            case ErrCodeUnauthorized:
                // 触发 401 响应
            }
        } else {
            // 非业务错误,走通用处理
            fmt.Println("未知错误:", err)
        }

        // 方式二:errors.Is 检查哨兵错误(适用于预定义变量)
        if errors.Is(err, ErrNotFound) {
            // 处理未找到错误
        }
    }
}
断言策略总结:
  • 使用 errors.As 提取自定义错误类型,获取结构化字段。
  • 使用 errors.Is 检查是否匹配某个哨兵错误(适用于包级别错误变量)。
  • 优先使用 errors.As 而非直接类型断言 err.(*BizError),因为 errors.As 会遍历错误链。
自定义错误类型与断言流程 errors.New / fmt.Errorf 自定义 BizError 类型 携带 Code / UserMsg / Err errors.As 提取 BizError switch 按 Code 分流 获取 UserMsg 返回前端 核心原则 • 自定义类型携带结构化字段,通过 errors.As 安全提取 • errors.New / fmt.Errorf 仅用于哨兵错误或临时包装 • 错误断言优先使用 errors.As 而非直接类型断言

3.6 实战案例:带上下文的订单错误处理

以下完整示例展示了如何在真实业务中组合使用自定义错误类型、错误包装和断言:

// 定义订单相关错误码
const (
    ErrOrderNotFound     = 20001
    ErrOrderPaid         = 20002
    ErrOrderCanceled     = 20003
)

// OrderError 订单错误
type OrderError struct {
    Code    int
    OrderID string
    Message string
    Err     error
}

func (e *OrderError) Error() string {
    return fmt.Sprintf("[order:%s] code=%d msg=%s", e.OrderID, e.Code, e.Message)
}

func (e *OrderError) Unwrap() error {
    return e.Err
}

// 业务函数:取消订单
func CancelOrder(orderID string) error {
    order, err := findOrder(orderID)
    if err != nil {
        return &OrderError{
            Code:    ErrOrderNotFound,
            OrderID: orderID,
            Message: "订单不存在",
            Err:     err,
        }
    }
    if order.Status == "paid" {
        return &OrderError{
            Code:    ErrOrderPaid,
            OrderID: orderID,
            Message: "已支付订单不可取消",
        }
    }
    // 执行取消逻辑...
    return nil
}

// 调用方:断言并处理
func handleCancelRequest(orderID string) {
    err := CancelOrder(orderID)
    if err != nil {
        var oErr *OrderError
        if errors.As(err, &oErr) {
            switch oErr.Code {
            case ErrOrderNotFound:
                // 返回 404
            case ErrOrderPaid:
                // 返回 400 并提示用户
            }
            log.Printf("订单错误: %s (order=%s)", oErr.Message, oErr.OrderID)
        } else {
            log.Printf("系统错误: %v", err)
        }
    }
}
本章要点回顾:
  • 自定义错误类型通过实现 Error() 和 Unwrap() 方法,同时满足接口和链式查找需求。
  • errors.New 和 fmt.Errorf 是基础工具,但不应替代自定义类型在业务层的主导地位。
  • 携带上下文信息时,优先使用结构体字段,其次考虑错误链包装,最后才是动态接口。
  • 错误断言使用 errors.As 提取自定义类型,使用 errors.Is 检查哨兵错误。