2、自定义错误类型:实现 error 接口、errors.New 与 fmt.Errorf、携带上下文的错误结构体
Go 语言的错误处理核心是 error 接口。任何实现了该接口的类型都可以作为错误值传递。本章将深入剖析如何自定义错误类型,对比标准库的两种快速构造方式,并重点讲解如何构建携带丰富上下文的错误结构体,为后续的错误码断言奠定基础。
2.1 error 接口与自定义类型
error 是 Go 内置的预声明接口,只包含一个方法:
type error interface {
Error() string
}
任何类型只要实现了 Error() string 方法,就自动满足 error 接口。这意味着我们可以创建任意复杂度的错误类型,而不仅仅是字符串。
核心原则:自定义错误类型应始终实现
Error() string 方法,并通常使用指针接收者以避免复制时丢失类型信息。
下面是一个基础的自定义错误类型示例:
// MyError 自定义错误类型
type MyError struct {
Code int
Message string
}
// Error 实现 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"}
}
2.2 errors.New 与 fmt.Errorf 的对比
标准库提供了两种快速创建简单错误的方式,但它们都不支持携带结构化字段。理解它们的差异有助于在合适的场景做出选择。
| 特性 | errors.New | fmt.Errorf |
|---|---|---|
| 基本功能 | 创建仅包含静态文本的错误 | 创建包含格式化文本的错误 |
| 格式化支持 | 不支持 | 支持 %s、%d、%v 等占位符 |
| 动态参数 | 无 | 可嵌入变量值 |
| 类型断言 | 无法提取结构化字段 | 无法提取结构化字段 |
| 适用场景 | 固定错误消息,如哨兵错误 | 需要拼接运行时值的临时错误 |
// errors.New 示例:哨兵错误
var ErrNotFound = errors.New("item not found")
// fmt.Errorf 示例:动态错误
userID := 1001
err := fmt.Errorf("user %d not found in database", userID)
注意:使用
fmt.Errorf 时,如果后续需要通过错误码断言提取字段,必须使用自定义错误类型。仅靠格式化字符串无法恢复结构化数据。
2.3 携带上下文的错误结构体
在实际项目中,错误往往需要携带多个维度的上下文信息,例如错误码、用户消息、内部错误原因、请求追踪 ID 等。自定义结构体是实现这一目标的最佳方式。
下面是一个生产级别的错误结构体设计:
// AppError 应用级错误结构体
type AppError struct {
Code int // 业务错误码
Message string // 面向用户的友好提示
Err error // 内部原始错误(可嵌套)
RequestID string // 请求追踪标识
Timestamp time.Time // 错误发生时间
}
// Error 实现 error 接口
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] code=%d, message=%s, err=%v",
e.Timestamp.Format(time.RFC3339), e.Code, e.Message, e.Err)
}
// Unwrap 支持 errors.Is 和 errors.As 链式查找
func (e *AppError) Unwrap() error {
return e.Err
}
// 工厂函数:快速构造带上下文的错误
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
RequestID: getRequestIDFromContext(), // 从 context 提取
Timestamp: time.Now(),
}
}
最佳实践:为自定义错误类型实现
Unwrap() error 方法,使其与 errors.Is 和 errors.As 兼容,便于在调用链中逐层解包和断言。
2.4 错误结构体设计模式对比
根据项目复杂度,可以选择不同的错误结构体设计模式:
| 模式 | 字段数量 | 可扩展性 | 适用项目 |
|---|---|---|---|
| 简单字段型 | 2-3 个 | 低 | 小型工具、脚本 |
| 标准上下文型 | 4-6 个 | 中 | Web 服务、API 网关 |
| 全量追踪型 | 8+ 个 | 高 | 微服务、分布式系统 |
2.5 错误结构体与断言的关系
自定义错误结构体的核心价值在于:通过类型断言或 errors.As 提取结构化字段,实现精确的错误码匹配。这是后续章节“错误码断言”的基础。
// 断言示例
err := someFunction()
var appErr *AppError
if errors.As(err, &appErr) {
// 成功提取 AppError 结构体
switch appErr.Code {
case 1001:
// 处理特定业务错误码
case 1002:
// 处理另一种错误
default:
// 兜底处理
}
}
关键结论:没有自定义错误结构体,就无法携带结构化错误码;没有结构化错误码,就无法实现精确的断言匹配。自定义错误类型是整个错误码断言体系的基石。
2.6 实战:构建带错误码的错误结构体
下面是一个完整的实战示例,展示如何定义、构造和使用带错误码的错误结构体:
// 定义业务错误码常量
const (
ErrCodeSuccess = 0
ErrCodeInvalidInput = 1001
ErrCodeUnauthorized = 1002
ErrCodeNotFound = 1003
ErrCodeInternalError = 5000
)
// BizError 业务错误结构体
type BizError struct {
Code int // 业务错误码
Message string // 错误描述
Detail string // 详细调试信息
}
func (e *BizError) Error() string {
return fmt.Sprintf("biz_error: code=%d, message=%s", e.Code, e.Message)
}
// 工厂函数
func NewBizError(code int, message string) *BizError {
return &BizError{Code: code, Message: message}
}
// 使用示例
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, NewBizError(ErrCodeInvalidInput, "invalid user id")
}
// 模拟数据库查询
user, found := db.FindUser(id)
if !found {
return nil, NewBizError(ErrCodeNotFound, "user not found")
}
return user, nil
}
// 调用方断言
user, err := GetUser(-1)
if err != nil {
var bizErr *BizError
if errors.As(err, &bizErr) {
fmt.Printf("错误码: %d, 消息: %s\n", bizErr.Code, bizErr.Message)
// 根据错误码做不同处理
}
}
推荐做法:将错误码定义为有意义的常量,并在工厂函数中统一构造。这样既保证了错误码的一致性,也方便后续的断言匹配和文档生成。
2.7 本章小结
- error 接口是 Go 错误处理的核心契约,任何实现了
Error() string的类型都是错误。 - errors.New 适用于静态哨兵错误,fmt.Errorf 适用于动态格式化错误,但两者都无法携带结构化字段。
- 自定义错误结构体可以携带错误码、消息、内部错误、追踪 ID 等上下文信息,是实现精确断言的基础。
- 为自定义错误类型实现 Unwrap 方法,使其与
errors.Is和errors.As兼容,便于链式解包。 - 通过 errors.As 进行类型断言,可以提取错误结构体中的字段,实现基于错误码的业务逻辑分支。