2. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、%w 动词与错误链
Go 语言的错误处理哲学与多数主流语言不同:它没有异常抛出机制,而是通过函数返回 error 接口值来传递错误。这种显式处理方式让错误传播路径清晰可追踪,但也要求开发者掌握一套严谨的构建与断言方法。本章从最底层的 error 接口开始,逐步深入到错误链的构建与解析,为后续安全错误码断言打下基础。
2.1 error 接口:一切错误的契约
error 是 Go 语言内置的预声明接口,定义在 builtin 包中,其契约极其简洁:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都自动满足 error 接口。这意味着你可以为自定义错误类型附加额外字段(如错误码、上下文信息),同时保持与标准错误处理流程的兼容。
| 特性 | 说明 |
|---|---|
| 接口方法数 | 1 个:Error() string |
| 返回值用途 | 人类可读的错误描述,不用于程序逻辑判断 |
| 零值语义 | nil 表示无错误,非 nil 表示有错误 |
| 可比较性 | 接口值可比较,但依赖动态类型和动态值 |
error 是一个接口,不是字符串。两个 error 值相等当且仅当它们的动态类型相同且动态值相等。直接比较 err == nil 是安全的,但比较两个非 nil 错误时需谨慎。
2.2 errors.New:最简错误构造器
errors.New 接受一个字符串常量,返回一个包含该字符串的 error 值。其内部实现是一个未导出的 errorString 结构体:
// 标准库实现示意
package errors
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
func New(text string) error {
return &errorString{text}
}
使用示例:
var ErrNotFound = errors.New("record not found")
func GetUser(id int) (*User, error) {
// ...
return nil, ErrNotFound
}
errors.New 返回的是指针类型 *errorString。每次调用都会分配一个新指针,因此 errors.New("x") == errors.New("x") 结果为 false。正确的做法是将错误值定义为包级变量(如 var ErrX = errors.New("x")),通过指针相等性做 sentinel 错误判断。
2.3 fmt.Errorf:带格式的错误构造
fmt.Errorf 在 errors.New 的基础上增加了格式化能力,支持 %s、%d、%v 等占位符:
func OpenFile(path string) error {
return fmt.Errorf("cannot open file %s: permission denied", path)
}
在 Go 1.13 之前,fmt.Errorf 只能生成一个扁平字符串,无法保留原始错误信息。调用方只能通过字符串匹配(如 strings.Contains)来识别错误类型,这种做法脆弱且不安全。
2.4 %w 动词与错误链
Go 1.13 引入了 %w 动词,允许 fmt.Errorf 将已有错误包装为新的错误,同时保留对原始错误的引用。被包装的错误形成一条“错误链”,可以通过 errors.Is 和 errors.As 沿链遍历。
import (
"errors"
"fmt"
)
var ErrPermission = errors.New("permission denied")
func ReadConfig(path string) error {
// 模拟权限错误
return fmt.Errorf("read config %s: %w", path, ErrPermission)
}
func main() {
err := ReadConfig("/etc/app.conf")
// errors.Is 沿错误链查找匹配的 sentinel 错误
if errors.Is(err, ErrPermission) {
fmt.Println("权限错误,需要提升权限")
}
// err.Error() 输出包含完整上下文
fmt.Println(err)
// 输出: read config /etc/app.conf: permission denied
}
%w 的行为规则:
- 只能用于
fmt.Errorf,且只能出现一次%w。 - 被包装的错误必须是非
nil的error接口值。 - 包装后的错误实现了
Unwrap() error方法,返回被包装的错误。 - 多次包装形成链式结构,
errors.Is和errors.As会递归遍历整条链。
%w 时,错误消息的格式建议采用 "外层上下文: %w" 风格,冒号后加空格。这样既保留了原始错误信息,又增加了调用栈上下文,便于调试和日志分析。
2.5 错误链的结构与遍历
下图展示了三次 %w 包装形成的错误链结构,以及 errors.Is 的遍历路径:
2.6 errors.Is 与 errors.As:链式断言
Go 1.13 标准库提供了两个核心函数用于错误链断言:
| 函数 | 签名 | 用途 | 匹配依据 |
|---|---|---|---|
errors.Is | Is(err, target error) bool | 判断错误链中是否存在与 target 相等的错误 | 接口值相等(类型+值) |
errors.As | As(err error, target interface{}) bool | 查找错误链中第一个可赋值给 target 类型的错误 | 类型匹配(可赋值) |
errors.Is 示例:
var ErrTimeout = errors.New("operation timed out")
func fetchData() error {
return fmt.Errorf("fetch: %w", ErrTimeout)
}
func handle() {
err := fetchData()
if errors.Is(err, ErrTimeout) {
// 触发重试逻辑
fmt.Println("超时,准备重试")
}
}
errors.As 示例:
type HTTPError struct {
Code int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}
func callAPI() error {
return fmt.Errorf("api call failed: %w", &HTTPError{Code: 503, Message: "Service Unavailable"})
}
func handleAPI() {
err := callAPI()
var httpErr *HTTPError
if errors.As(err, &httpErr) {
// 提取 HTTP 状态码做针对性处理
fmt.Printf("HTTP 错误码: %d\n", httpErr.Code)
}
}
- 使用
errors.Is判断 sentinel 错误(包级变量定义的错误)。 - 使用
errors.As提取自定义错误类型中的结构化信息。 - 避免在业务逻辑中依赖
err.Error()字符串匹配,那会引入脆弱性。
2.7 自定义 Unwrap 方法
除了 fmt.Errorf 的 %w,你也可以在自定义错误类型上实现 Unwrap() error 方法,使其参与错误链:
type WrappedError struct {
Msg string
Err error
}
func (w *WrappedError) Error() string {
return fmt.Sprintf("%s: %v", w.Msg, w.Err)
}
func (w *WrappedError) Unwrap() error {
return w.Err
}
func process() error {
return &WrappedError{
Msg: "process failed",
Err: ErrPermission,
}
}
func main() {
err := process()
fmt.Println(errors.Is(err, ErrPermission)) // true
}
多个包装层可以形成更复杂的链,errors.Is 会递归调用每个节点的 Unwrap 方法直到链尾或匹配成功。
2.8 错误链与安全编码
从安全编码角度看,错误链设计需要遵循以下原则:
- 不泄露敏感信息: 外层错误消息应使用通用描述,避免将内部路径、SQL 语句、堆栈细节暴露给用户。敏感信息应放在
Unwrap的内层错误中,仅用于日志记录。 - 不破坏断言契约: 使用
%w包装时,确保被包装的错误是包级导出的 sentinel 错误或公开的自定义错误类型,以便调用方通过errors.Is/errors.As可靠断言。 - 避免循环包装: 不要将同一个错误实例反复包装到自身,会导致
errors.Is陷入无限循环。
Error() 方法中返回包含密码、令牌、密钥或用户个人身份信息的字符串。错误消息可能被记录到日志系统、返回给 API 客户端或显示在用户界面上,信息泄露将直接导致安全漏洞。
2.9 本章小结
Go 的错误处理基础围绕 error 接口展开:errors.New 构造不可变的 sentinel 错误,fmt.Errorf 配合 %w 动词构建可遍历的错误链,errors.Is 和 errors.As 提供链式断言能力。掌握这些基础是后续实现安全错误码断言的前提——错误链的 Unwrap 机制正是自定义错误码断言器能够沿链查找匹配码的底层支撑。