2. error 接口:Go 内置 error 接口定义、实现自定义错误类型、errors.New 与 fmt.Errorf 基础用法

在 Go 语言中,错误处理不是通过异常机制(try-catch)实现的,而是通过一个轻量级的 接口(interface) 来传递错误值。这个接口就是 Go 标准库中定义的 error 接口。理解它的定义、实现方式以及基础构造工具,是掌握 Go 业务错误处理的基石。

2.1 内置 error 接口定义

Go 的 error 接口定义极其简洁,只包含一个方法:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型,都自动满足 error 接口。这意味着 Go 的错误本质上是一个 值,可以像普通值一样被赋值、传递、比较和返回。

关键认知: Go 的 error 是一个接口,而不是一个类或结构体。任何类型只要拥有 Error() string 方法,就可以作为错误使用。这种设计使得错误处理非常灵活,也避免了传统异常机制对控制流的侵入。

2.2 实现自定义错误类型

由于 error 是一个接口,我们可以通过定义结构体并实现 Error() 方法来创建自定义错误类型。这是构建业务领域错误的基础。

2.2.1 基础自定义错误

// 定义一个简单的错误结构体
type MyError struct {
    Code    int
    Message string
}

// 实现 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"}
}

func main() {
    err := doSomething()
    if err != nil {
        // 类型断言获取具体错误信息
        if myErr, ok := err.(*MyError); ok {
            fmt.Printf("业务错误码: %d\n", myErr.Code)
        }
    }
}

2.2.2 带上下文的业务错误

在实际业务系统中,错误通常需要携带更多上下文信息,如用户 ID、请求 ID、时间戳等。

type OrderError struct {
    OrderID string
    UserID  string
    Stage   string
    Err     error
}

func (e *OrderError) Error() string {
    return fmt.Sprintf("order_id=%s, user_id=%s, stage=%s, err=%v",
        e.OrderID, e.UserID, e.Stage, e.Err)
}

func (e *OrderError) Unwrap() error {
    return e.Err
}
最佳实践: 自定义错误类型应尽量保持 不可变(immutable),避免在错误传播过程中被意外修改。同时,建议实现 Unwrap() 方法以支持错误链(error wrapping),这在后续章节会详细讲解。

2.3 errors.New 与 fmt.Errorf 基础用法

Go 标准库提供了两个最常用的错误构造工具:errors.New 和 fmt.Errorf。它们适用于不同场景。

函数 签名 特点 适用场景
errors.New func New(text string) error 返回一个包含固定文本的简单错误;内部使用 errorString 结构体 静态错误消息,无需格式化参数
fmt.Errorf func Errorf(format string, a ...any) error 支持 fmt.Printf 风格的格式化;支持 %w 动词进行错误包装(Go 1.13+) 需要动态格式化错误消息,或需要包装底层错误

2.3.1 errors.New 使用示例

import "errors"

var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrInvalidInput = errors.New("invalid input parameters")
)

func getUser(id string) (User, error) {
    if id == "" {
        return User{}, ErrInvalidInput
    }
    // ... 查询逻辑
    return User{}, ErrNotFound
}
注意: 使用 errors.New 定义的错误变量通常声明为包级别的 全局变量,并使用 Err 前缀命名。这样做的好处是调用方可以通过 == 直接比较错误值,实现精确的错误匹配。但要注意,如果错误需要携带动态数据,则应使用自定义错误类型。

2.3.2 fmt.Errorf 使用示例

import "fmt"

func processOrder(orderID string, amount float64) error {
    if amount <= 0 {
        // 简单格式化,不包装底层错误
        return fmt.Errorf("invalid amount %.2f for order %s", amount, orderID)
    }
    // ... 处理逻辑
    return nil
}

// 使用 %w 包装错误(Go 1.13+)
func validateOrder(orderID string) error {
    baseErr := errors.New("order validation failed")
    return fmt.Errorf("order %s: %w", orderID, baseErr)
}

2.4 三种错误构造方式的对比

为了帮助你在实际开发中做出正确选择,下表对比了三种主要错误构造方式:

方式 是否支持动态数据 是否支持错误包装 是否支持类型断言 推荐场景
errors.New 否(静态文本) 否 否(内部类型不可导出) 预定义的、无参数的错误常量
fmt.Errorf(无 %w) 是(格式化) 否 否 需要动态消息但不需要错误链的场景
fmt.Errorf(含 %w) 是(格式化) 是(包装一个错误) 否(但可通过 errors.Is/As 解包) 需要保留原始错误链的场景
自定义错误类型 是(结构体字段) 是(需手动实现 Unwrap) 是(通过类型断言) 需要携带结构化业务数据、支持类型断言的高级场景

2.5 错误构造与传播流程

下面的 SVG 图展示了从错误产生到最终处理的完整流程,以及不同构造方式在其中的位置:

错误构造与传播流程 错误产生 errors.New(静态文本) fmt.Errorf(格式化) 自定义错误类型 错误传播(return) 错误处理 == 比较(errors.New) errors.Is/As(包装错误) 类型断言(自定义类型)

2.6 选择指南

在实际业务代码中,如何选择错误构造方式?以下是一个简单的决策流程:

  1. 错误消息是否完全静态? → 使用 errors.New,定义为包级别变量。
  2. 需要动态格式化消息,但不需要包装底层错误? → 使用 fmt.Errorf(不含 %w)。
  3. 需要保留原始错误链(如数据库错误、网络错误)? → 使用 fmt.Errorf 配合 %w。
  4. 需要携带结构化业务数据(错误码、用户ID、订单ID等)? → 定义自定义错误类型。
  5. 需要调用方通过类型断言获取具体错误信息? → 必须使用自定义错误类型。
进阶提示: 在大型业务系统中,建议将 errors.New 用于定义 哨兵错误(sentinel errors),将自定义错误类型用于 领域错误(domain errors)。哨兵错误用于表示“发生了某种已知类型的错误”,而领域错误则携带了业务上下文。两者结合使用,可以构建出既清晰又灵活的错误处理体系。