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 图展示了从错误产生到最终处理的完整流程,以及不同构造方式在其中的位置:
2.6 选择指南
在实际业务代码中,如何选择错误构造方式?以下是一个简单的决策流程:
- 错误消息是否完全静态? → 使用
errors.New,定义为包级别变量。 - 需要动态格式化消息,但不需要包装底层错误? → 使用
fmt.Errorf(不含%w)。 - 需要保留原始错误链(如数据库错误、网络错误)? → 使用
fmt.Errorf配合%w。 - 需要携带结构化业务数据(错误码、用户ID、订单ID等)? → 定义自定义错误类型。
- 需要调用方通过类型断言获取具体错误信息? → 必须使用自定义错误类型。
进阶提示: 在大型业务系统中,建议将
errors.New 用于定义 哨兵错误(sentinel errors),将自定义错误类型用于 领域错误(domain errors)。哨兵错误用于表示“发生了某种已知类型的错误”,而领域错误则携带了业务上下文。两者结合使用,可以构建出既清晰又灵活的错误处理体系。