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 错误处理的核心数据流:从错误构造到逐层传递,再到最终断言处理。
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 进行错误链遍历。