3. 标准库 error 接口:定义、创建与格式化
在 Go 语言中,error 是一个预定义的内置接口,是错误处理的核心抽象。任何实现了 Error() string 方法的类型都可以作为错误值传递。理解 error 接口的定义、创建简单错误(errors.New)以及带格式错误(fmt.Errorf),是构建可审计、可搜索错误码断言体系的基础。
3.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() string |
| 零值语义 | nil 表示无错误 |
| 可扩展性 | 自定义类型可实现该接口,携带额外字段 |
| 不可变性 | 标准库错误创建后,错误消息字符串不可修改 |
审计要点: 在代码审计中,应检查自定义错误类型是否完整实现了
error 接口,以及 Error() 方法是否泄露了敏感信息(如数据库连接串、文件路径等)。
3.2 创建简单错误:errors.New
errors.New 是最基础的错误创建方式,它接收一个字符串参数,返回一个包含该字符串的 error 值。其内部实现返回一个 errorString 结构体指针,该结构体只存储了错误消息。
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
使用示例:
var ErrNotFound = errors.New("record not found")
var ErrPermissionDenied = errors.New("permission denied")
func FindUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrNotFound
}
// ...
}
审计陷阱: 每次调用
errors.New 都会分配一个新的 errorString 指针。如果相同的错误消息在热路径上被频繁创建,会导致不必要的内存分配。最佳实践是将常用错误声明为包级变量(如 var ErrXxx = errors.New(...)),以便通过 == 比较进行断言。
3.3 带格式错误:fmt.Errorf
fmt.Errorf 在 errors.New 的基础上增加了格式化能力,允许你使用 fmt.Sprintf 风格的占位符构建动态错误消息。从 Go 1.13 开始,fmt.Errorf 还支持通过 %w 动词包装错误,从而支持错误链。
// 基本格式化错误
err := fmt.Errorf("user %d not found in database %s", userID, dbName)
// 包装错误(Go 1.13+)
if err := doSomething(); err != nil {
return fmt.Errorf("doSomething failed: %w", err)
}
// 多个错误包装
err1 := errors.New("first error")
err2 := errors.New("second error")
combined := fmt.Errorf("multiple errors: %w; %w", err1, err2)
| 动词 | 作用 | 审计关注点 |
|---|---|---|
%s / %v |
格式化字符串/值 | 可能泄露敏感数据 |
%w |
包装错误(支持 errors.Is/errors.As) |
确保包装链完整,不丢失根因 |
%+v |
带字段名的结构体输出 | 可能暴露内部结构 |
错误码断言最佳实践: 在审计中,应优先使用
fmt.Errorf("... %w", err) 包装底层错误,而不是拼接字符串。这样可以保留错误链,使上层代码能够通过 errors.Is 或 errors.As 精确匹配根因错误,实现可靠的错误码断言。
3.4 三种创建方式对比
| 创建方式 | 适用场景 | 是否支持格式化 | 是否支持错误包装 | 内存分配 |
|---|---|---|---|---|
errors.New |
静态、预定义的错误常量 | 否 | 否 | 每次调用分配一次 |
fmt.Errorf(无 %w) |
动态消息,不需要错误链 | 是 | 否 | 每次调用分配一次 |
fmt.Errorf(含 %w) |
需要保留错误链的场景 | 是 | 是 | 包装结构体额外分配 |
3.5 错误创建流程与断言关系
下图展示了从错误创建到错误码断言的核心流程,帮助理解 errors.New 和 fmt.Errorf 在审计基线中的位置:
3.6 审计基线检查清单
- 错误常量化: 检查包内是否将常用错误声明为
var ErrXxx = errors.New("...")形式,避免在函数内重复创建相同错误。 - 格式化安全: 审计
fmt.Errorf的格式化参数,确保不将用户输入、敏感数据直接作为格式化字符串(防止格式化注入)。 - 错误包装完整性: 在多层调用中,检查是否每一层都使用
%w包装了底层错误,避免错误链断裂导致errors.Is无法匹配根因。 - 断言方式选择: 对于预定义错误常量,使用
==或errors.Is;对于动态错误,优先使用errors.Is而非字符串包含判断。