2. 基础错误类型:error 接口的定义、errors.New() 创建简单错误、fmt.Errorf() 格式化错误、哨兵错误(Sentinel Error)模式
在 Go 语言中,错误处理的核心是 error 接口。理解这个接口的定义以及如何创建和识别错误,是构建统一错误处理模式的基石。本章将深入剖析 Go 语言的基础错误类型,从最简单的错误创建到哨兵错误模式,为后续章节的业务层错误封装打下坚实基础。
2.1 error 接口的定义
Go 语言通过一个预定义的接口类型 error 来表示错误状态。这个接口极其简洁,只包含一个方法:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都可以作为错误值。这意味着 Go 的错误处理是类型安全的,并且非常灵活。你可以创建任意复杂的错误类型,只要它满足这个接口。
核心要点:
error 是一个内置接口,而非异常机制。函数通过返回 error 值来告知调用方操作是否成功。调用方必须显式检查错误,这强制了健壮的错误处理习惯。
2.2 errors.New() 创建简单错误
标准库 errors 包提供了 New() 函数,用于创建包含静态文本的简单错误。这是最基础、最常用的错误创建方式。
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("发生错误:", err)
} else {
fmt.Println("结果:", result)
}
}
特点:
- 简单直接: 适用于不需要额外上下文信息的静态错误消息。
- 不可变: 创建的
error值是不可变的,其内部错误消息在创建后无法修改。 - 性能高效: 内部实现非常轻量,不涉及格式化操作。
2.3 fmt.Errorf() 格式化错误
当需要在错误消息中嵌入动态信息(如变量值、参数等)时,fmt.Errorf() 是更合适的选择。它支持 fmt 包的所有格式化动词。
package main
import (
"fmt"
"os"
)
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 使用 %w 动词包装原始错误(Go 1.13+)
return fmt.Errorf("打开配置文件 %s 失败: %w", path, err)
}
return nil
}
func main() {
err := openConfigFile("/etc/myapp/config.yaml")
if err != nil {
fmt.Println(err)
// 输出: 打开配置文件 /etc/myapp/config.yaml 失败: open /etc/myapp/config.yaml: no such file or directory
}
}
关键区别:
fmt.Errorf() 与 errors.New() 的主要区别在于格式化能力。前者可以创建包含动态内容的错误,而后者只能创建静态错误。从 Go 1.13 开始,fmt.Errorf() 支持 %w 动词来包装错误,这是错误链的基础。
2.4 哨兵错误 (Sentinel Error) 模式
哨兵错误是预定义的、命名的错误变量,用于表示特定的、可预期的错误条件。调用方可以通过 == 运算符直接比较错误值,从而做出不同的处理决策。
定义哨兵错误:
package main
import (
"errors"
"fmt"
)
// 定义哨兵错误
var ErrNotFound = errors.New("资源未找到")
var ErrPermissionDenied = errors.New("权限不足")
var ErrTimeout = errors.New("操作超时")
func queryResource(id int) error {
if id < 0 {
return ErrNotFound
}
if id == 0 {
return ErrPermissionDenied
}
// ... 正常处理
return nil
}
func main() {
err := queryResource(-1)
if errors.Is(err, ErrNotFound) {
fmt.Println("处理未找到错误: 返回 404")
} else if errors.Is(err, ErrPermissionDenied) {
fmt.Println("处理权限错误: 返回 403")
} else if err != nil {
fmt.Println("其他错误:", err)
}
}
使用建议: 哨兵错误适用于包级别或模块级别的、调用方需要明确区分的错误场景。但应避免过度使用,因为每个哨兵错误都成为包公共 API 的一部分,增加了耦合。在业务层,更推荐使用自定义错误类型来携带更多上下文。
2.5 三种错误创建方式对比
| 特性 | errors.New() | fmt.Errorf() | 哨兵错误 |
|---|---|---|---|
| 创建方式 | 静态文本 | 格式化文本 | 预定义变量 |
| 动态内容 | 不支持 | 支持 | 不支持(静态) |
| 错误比较 | == 或 errors.Is() |
errors.Is()(需 %w) |
== 或 errors.Is() |
| 适用场景 | 简单、固定的错误消息 | 需要嵌入变量或包装错误 | 可预期的、需要被调用方区分的错误 |
| 性能 | 高 | 中(涉及格式化) | 高 |
2.6 错误处理流程与哨兵错误模式
下面的 SVG 图展示了在业务层中,如何使用哨兵错误模式进行错误判断和分支处理。调用方通过 errors.Is() 函数(或 ==)与预定义的哨兵错误进行比较,从而执行不同的错误处理逻辑。
2.7 哨兵错误与自定义错误类型的对比
在业务层设计中,哨兵错误和自定义错误类型各有适用场景。下面的表格可以帮助你做出选择:
| 维度 | 哨兵错误 | 自定义错误类型 |
|---|---|---|
| 定义方式 | var ErrX = errors.New("...") |
type MyError struct { ... } |
| 携带上下文 | 有限(仅静态消息) | 丰富(可包含字段、方法) |
| 比较方式 | == 或 errors.Is() |
errors.As() 类型断言 |
| 适用层级 | 包边界、库 API | 业务层、领域层 |
| 扩展性 | 低(新增哨兵需改 API) | 高(可继承、组合) |
| 典型场景 | io.EOF, sql.ErrNoRows |
业务错误码、验证错误 |
最佳实践: 在业务层中,建议将哨兵错误用于包级别的、调用方必须明确处理的“契约式”错误(如“资源不存在”、“权限不足”)。对于需要携带动态数据(如字段名、期望值、实际值)的错误,应优先使用自定义错误类型,并结合
fmt.Errorf() 的 %w 动词进行包装,以保留错误链。
2.8 本章小结
- error 接口 是 Go 错误处理的核心,只要求实现
Error() string方法。 - errors.New() 用于创建静态、简单的错误消息。
- fmt.Errorf() 支持格式化错误消息,并通过
%w动词实现错误包装。 - 哨兵错误模式 通过预定义的错误变量,使调用方能够精确区分错误类型,是构建可预测错误处理流程的重要工具。
- 在业务层设计中,应根据场景选择哨兵错误或自定义错误类型,并遵循“契约式”错误设计原则。