2. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、自定义 error 类型
在 Go 语言中,错误处理不是通过异常(try-catch)机制实现的,而是通过显式返回 error 值来传递错误状态。这种设计哲学要求开发者将错误视为普通值,在每个可能出错的调用点主动检查并处理。本章将系统讲解 Go 错误处理的核心基础:error 接口、标准库提供的错误构造方法以及自定义错误类型。
2.1 error 接口:一切错误的契约
Go 语言内置的 error 接口是所有错误类型的统一抽象。它只定义了一个方法:Error() string,用于返回错误的描述信息。
// Go 标准库定义
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都自动满足 error 接口,这体现了 Go 语言“隐式接口”的设计理念。函数或方法通过返回 error 类型来告知调用方操作是否成功。
核心要点:在 Go 中,
nil 表示没有错误,非 nil 的 error 值表示发生了错误。调用方必须检查返回值中的 error 是否为 nil。
2.2 构造简单错误:errors.New 与 fmt.Errorf
标准库提供了两种最常用的方式来创建简单的错误值:errors.New 和 fmt.Errorf。
| 函数 | 签名 | 特点 | 适用场景 |
|---|---|---|---|
errors.New |
func New(text string) error |
仅接受静态字符串,返回一个包含该文本的 error 值 |
固定错误消息,无需格式化 |
fmt.Errorf |
func Errorf(format string, a ...any) error |
支持 fmt.Printf 风格的格式化占位符,返回格式化后的 error 值 |
需要动态拼接错误信息,如包含变量值 |
// 使用 errors.New
var ErrNotFound = errors.New("record not found")
// 使用 fmt.Errorf
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero: cannot divide %d by %d", a, b)
}
return a / b, nil
}
最佳实践:对于包级别可导出的哨兵错误(Sentinel Error),优先使用
errors.New 并声明为 var 变量,方便调用方使用 == 或 errors.Is 进行断言。
2.3 自定义 error 类型:携带更多上下文
当错误需要携带结构化信息(如错误码、内部原因、堆栈信息)时,自定义 error 类型是更好的选择。通过实现 Error() string 方法,任何结构体都可以成为 error 类型。
// 自定义错误类型:携带错误码和内部原因
type MyError struct {
Code int
Message string
Err error // 内部包装的错误
}
// 实现 error 接口
func (e *MyError) Error() string {
if e.Err != nil {
return fmt.Sprintf("code=%d, message=%s, cause=%v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}
// 使用示例
func Process(id int) error {
if id < 0 {
return &MyError{
Code: 400,
Message: "invalid id",
Err: fmt.Errorf("id must be non-negative, got %d", id),
}
}
// ... 正常处理
return nil
}
注意:自定义错误类型通常使用指针接收者实现
Error() 方法,以确保接口断言时类型匹配。同时,自定义错误类型应提供 Unwrap() 方法以支持错误链(将在后续章节详述)。
2.4 三种错误构造方式对比
下表从多个维度对比了三种错误构造方式的差异,帮助你在实际开发中做出合适的选择。
| 维度 | errors.New | fmt.Errorf | 自定义 error 类型 |
|---|---|---|---|
| 信息丰富度 | 仅静态文本 | 支持格式化 | 完全可控,可包含任意字段 |
| 类型断言 | 无法断言(无类型) | 无法断言(无类型) | 支持 errors.As 断言 |
| 错误链支持 | 不支持 | Go 1.13+ 支持 %w |
需手动实现 Unwrap() |
| 性能开销 | 低 | 中(格式化开销) | 中到高(结构体分配) |
| 适用场景 | 哨兵错误、常量错误 | 快速构造带变量的错误 | 需要错误码、分类、堆栈等结构化信息 |
2.5 错误处理基础流程
下面的 SVG 图展示了 Go 程序中典型的错误处理流程:从函数返回错误,到调用方检查并做出相应处理。
2.6 实战:构建一个带错误码的自定义错误
以下示例展示了一个完整的自定义错误类型实现,包含错误码、用户消息和内部原因,并提供了 Unwrap() 方法以支持错误链。
package main
import (
"errors"
"fmt"
)
// AppError 应用级错误,包含错误码和内部原因
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 预定义错误码
const (
ErrCodeNotFound = 404
ErrCodeInternal = 500
ErrCodeValidation = 400
)
// 哨兵错误
var ErrNotFound = errors.New("resource not found")
// GetUser 模拟获取用户信息
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, &AppError{
Code: ErrCodeValidation,
Message: "invalid user id",
Err: fmt.Errorf("id must be positive, got %d", id),
}
}
if id > 100 {
return nil, &AppError{
Code: ErrCodeNotFound,
Message: "user not found",
Err: ErrNotFound,
}
}
return &User{ID: id, Name: "Alice"}, nil
}
type User struct {
ID int
Name string
}
func main() {
_, err := GetUser(-1)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("错误码: %d\n", appErr.Code)
fmt.Printf("消息: %s\n", appErr.Message)
fmt.Printf("内部原因: %v\n", appErr.Err)
}
}
}
关键点:通过
errors.As 函数可以将错误断言为自定义类型,从而提取结构化字段。这是 Go 错误码断言的核心机制之一,后续章节将深入展开。
2.7 本章小结
- error 接口是 Go 错误处理的基石,任何实现了
Error() string方法的类型都是error。 - errors.New 适用于构造静态哨兵错误,fmt.Errorf 适用于快速格式化错误消息。
- 自定义 error 类型通过结构体字段携带错误码、内部原因等结构化信息,支持
errors.As断言。 - 选择哪种构造方式取决于是否需要类型断言、错误链支持以及信息丰富度。