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 在审计基线中的位置:

错误创建与断言流程 errors.New("msg") fmt.Errorf("fmt %s", v) fmt.Errorf("... %w", err) *errors.errorString *errors.errorString *fmt.wrapError err == ErrXxx err.Error() 包含子串 errors.Is(err, ErrXxx) 审计结论:推荐使用 errors.New 定义常量 + fmt.Errorf %w 包装 + errors.Is 断言

3.6 审计基线检查清单

  • 错误常量化: 检查包内是否将常用错误声明为 var ErrXxx = errors.New("...") 形式,避免在函数内重复创建相同错误。
  • 格式化安全: 审计 fmt.Errorf 的格式化参数,确保不将用户输入、敏感数据直接作为格式化字符串(防止格式化注入)。
  • 错误包装完整性: 在多层调用中,检查是否每一层都使用 %w 包装了底层错误,避免错误链断裂导致 errors.Is 无法匹配根因。
  • 断言方式选择: 对于预定义错误常量,使用 == 或 errors.Is;对于动态错误,优先使用 errors.Is 而非字符串包含判断。