1. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、自定义错误类型

在 Go 语言中,错误处理不是通过异常(try-catch)机制实现的,而是通过显式返回 error 接口值来传递错误信息。这种设计哲学要求开发者将错误视为普通值,在调用链中逐层检查、包装或转换。理解 Go 的错误处理基础,是构建健壮分布式系统和实现可靠发布回滚的前提。

1.1 error 接口:一切错误的根

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

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误值传递。这意味着错误可以是结构体、指针、甚至函数类型,只要它满足这个契约。这种极简设计让错误处理变得灵活且可组合。

关键认知: Go 中的 error 是值,不是控制流。你可以像处理普通数据一样存储、比较、包装和传递错误。

1.2 快速构造错误:errors.New 与 fmt.Errorf

标准库提供了两种最常用的错误构造方式:

函数 签名 适用场景 示例
errors.New func New(text string) error 静态错误消息,无需格式化 errors.New("record not found")
fmt.Errorf func Errorf(format string, a ...any) error 动态错误消息,需要格式化参数 fmt.Errorf("user %d: %w", id, err)

使用要点:

  • errors.New 返回的错误值不可比较(不推荐用 == 判断),除非你使用同一个变量。
  • fmt.Errorf 支持 %w 动词(Go 1.13+)来包装已有错误,形成错误链。
  • 不要用 fmt.Errorf 拼接字符串来构造静态错误,应优先使用 errors.New 以保持语义清晰。
// 基础用法
var ErrNotFound = errors.New("record not found")

func GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid user id: %d", id)
    }
    // ... 查询逻辑
    return nil, ErrNotFound
}

1.3 自定义错误类型:携带更多上下文

当错误需要携带结构化信息(如错误码、HTTP 状态码、内部原因)时,自定义错误类型是更好的选择。通过实现 Error() string 方法,你可以让错误携带业务语义。

// 自定义错误类型:携带错误码和内部原因
type AppError struct {
    Code    int    // 业务错误码,如 1001
    Message string // 用户可读的消息
    Err     error  // 内部包装的原始错误
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("code=%d, msg=%s, cause=%v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Message)
}

// 使用示例
func ProcessOrder(orderID string) error {
    if orderID == "" {
        return &AppError{
            Code:    4001,
            Message: "order ID is required",
            Err:     nil,
        }
    }
    // ... 处理逻辑
    return nil
}
实战建议: 在分布式系统中,自定义错误类型应至少包含:错误码(用于断言和监控)、可读消息(用于日志和用户展示)、内部原因(用于根因分析)。这为后续的“错误码断言”和“发布回滚决策”打下数据基础。

1.4 错误处理模式对比

模式 优点 缺点 适用场景
errors.New 简单、零依赖 无法携带结构化数据 简单哨兵错误
fmt.Errorf 支持格式化、支持 %w 包装 包装后难以直接提取原始错误 中间层包装错误
自定义类型 携带丰富上下文、支持类型断言 需要额外定义类型和接口 业务错误码、API 响应、领域错误

1.5 错误处理核心流程

下图展示了 Go 错误处理的核心数据流:从错误构造到逐层传递,再到最终断言处理。

Go 错误处理核心流程 errors.New fmt.Errorf 自定义错误类型 逐层返回 / 包装(%w) / 携带上下文 类型断言 / errors.Is / errors.As 日志记录 / 监控告警 / 回滚决策 构造阶段:选择合适的方式创建错误 传递阶段:保持错误链完整,不吞没错误 断言阶段:根据错误类型/错误码做出决策

1.6 错误断言基础:errors.Is 与 errors.As

Go 1.13 引入了两个关键函数用于错误链的断言:

  • errors.Is(err, target):判断错误链中是否存在与 target 相等的错误(基于 == 比较或 Is() 方法)。
  • errors.As(err, target):在错误链中查找第一个可以赋值给 target 的错误类型(基于类型断言或 As() 方法)。
// errors.Is 示例:判断是否为哨兵错误
if errors.Is(err, ErrNotFound) {
    // 返回 404 响应
}

// errors.As 示例:提取自定义错误类型
var appErr *AppError
if errors.As(err, &appErr) {
    // 根据 appErr.Code 做业务决策
    switch appErr.Code {
    case 4001:
        // 参数校验失败
    case 5001:
        // 内部服务错误,触发回滚
    }
}
常见陷阱: 不要用 == 直接比较 fmt.Errorf 包装后的错误,因为包装会创建新的错误值。始终使用 errors.Is 或 errors.As 进行错误链遍历。

1.7 本章小结