1. Go 错误处理基础:error 接口、errors.New、fmt.Errorf 的基本用法

在 Go 语言中,错误处理不是通过异常机制实现的,而是通过显式的返回值传递。这种设计哲学要求开发者将错误视为普通值,在每个可能出错的调用点主动检查。本章从最底层的 error 接口开始,逐步构建你对 Go 错误处理体系的理解。

1.1 error 接口:一切错误的根类型

Go 语言内置的 error 是一个预声明接口,定义在标准库的 builtin 包中。任何实现了该接口的类型都可以作为错误值传递。

// error 接口定义(builtin 包)
type error interface {
    Error() string
}

这个接口只有一个方法 Error() string,返回错误的描述字符串。这意味着:任何拥有 Error() string 方法的类型都自动满足 error 接口,无需显式声明 implements。

设计意图:Go 将错误定义为接口而非类,使得错误类型可以无限扩展,同时保持最小的契约约束。这是 Go 组合式设计思想的典型体现。

1.2 errors.New:创建最简单的错误

errors.New 是标准库提供的最基础错误构造函数。它接收一个字符串参数,返回一个包含该字符串的 error 值。

import "errors"

var ErrNotFound = errors.New("record not found")
var ErrPermission = errors.New("permission denied")

func FindUser(id int) (User, error) {
    if id <= 0 {
        return User{}, ErrNotFound
    }
    // ... 正常查找逻辑
}

使用 errors.New 创建的错误值具有以下特性:

特性 说明
不可变性 错误字符串在创建后不可修改
可比较性 相同字符串创建的 error 值可以通过 == 比较
唯一性 每次调用 errors.New 都会分配新的内存地址
无上下文 仅包含静态字符串,无法附加调用栈或动态信息
常见陷阱:不要将 errors.New 放在循环或频繁调用的函数内部。每次调用都会分配新对象,可能造成不必要的内存压力。推荐将静态错误声明为包级变量。

1.3 fmt.Errorf:带格式的错误构造

当错误信息需要包含动态值时,fmt.Errorf 是比 errors.New 更灵活的选择。它支持 fmt 包的格式化动词,允许将变量嵌入错误描述中。

import "fmt"

func OpenFile(path string) (*os.File, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open file %s: %w", path, err)
    }
    return f, nil
}

fmt.Errorf 支持三种格式化行为:

  • %s / %v:将值格式化为字符串,返回一个新的错误,不保留原始错误链。
  • %w(Go 1.13+):将错误包装进新错误,保留原始错误以便后续通过 errors.Is 或 errors.As 解包。
  • %+v(配合 github.com/pkg/errors):输出带调用栈的详细错误。
最佳实践:在服务层或边界层使用 %w 包装错误,保留底层错误的类型信息;在顶层处理时使用 errors.Is 判断错误类型,避免直接比较字符串。

1.4 三种构造方式的对比

构造方式 适用场景 是否支持格式化 是否支持错误链 性能
errors.New 静态、预定义的错误常量 否 否 高(无额外格式化开销)
fmt.Errorf(无 %w) 需要动态信息的临时错误 是 否 中
fmt.Errorf(含 %w) 需要保留原始错误链的包装 是 是 中(额外包装开销)

1.5 错误处理的基本模式

Go 社区形成了两种主流的错误处理模式,理解它们的差异有助于你在不同场景下做出正确选择。

// 模式一:哨兵错误(Sentinel Error)
var ErrNoRows = errors.New("sql: no rows in result set")

func QueryRow(db *sql.DB, id int) (Row, error) {
    // ...
    if err == sql.ErrNoRows {
        return Row{}, ErrNoRows
    }
    // ...
}

// 模式二:错误类型(Error Type)
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}

func Validate(input string) error {
    if len(input) == 0 {
        return &ValidationError{Field: "input", Message: "cannot be empty"}
    }
    return nil
}
选择建议:哨兵错误适合表示“预期内的失败”(如文件不存在、记录未找到);错误类型适合需要携带额外结构化信息的场景(如字段校验错误、网络状态码)。在报表系统中,建议优先使用哨兵错误定义领域内的已知失败场景。

1.6 错误处理的核心流程

下面的 SVG 图展示了 Go 错误处理从创建到消费的完整生命周期,帮助你建立全局视角。

Go 错误处理核心流程 Go 错误处理核心流程 错误创建 errors.New fmt.Errorf 错误传递 直接返回 包装返回 错误消费 if err != nil errors.Is/As 关键原则 错误是值,不是异常。每个可能出错的调用点都应显式处理错误,避免静默吞没。

1.7 报表场景中的错误处理示例

在报表系统中,错误处理需要兼顾可读性和可追踪性。以下是一个典型的报表生成函数,展示了三种构造方式的协同使用。

package report

import (
    "errors"
    "fmt"
)

// 哨兵错误:定义领域内已知的失败场景
var (
    ErrNoData      = errors.New("report: no data available for the given period")
    ErrInvalidDate = errors.New("report: invalid date range")
)

// ReportError 自定义错误类型:携带额外上下文
type ReportError struct {
    Code    int
    Message string
    Err     error
}

func (e *ReportError) Error() string {
    return fmt.Sprintf("report error [%d]: %s", e.Code, e.Message)
}

func (e *ReportError) Unwrap() error {
    return e.Err
}

// GenerateReport 报表生成函数
func GenerateReport(start, end string) ([]byte, error) {
    if start > end {
        return nil, fmt.Errorf("%w: start=%s end=%s", ErrInvalidDate, start, end)
    }

    data, err := fetchData(start, end)
    if err != nil {
        return nil, &ReportError{
            Code:    1001,
            Message: "failed to fetch report data",
            Err:     err,
        }
    }

    if len(data) == 0 {
        return nil, ErrNoData
    }

    return renderReport(data)
}
报表错误处理要点:
  • 使用哨兵错误(ErrNoData、ErrInvalidDate)定义领域内可预期的失败场景,便于调用方通过 errors.Is 精确判断。
  • 使用 fmt.Errorf 的 %w 动词包装底层错误,保留错误链以便后续排查。
  • 自定义错误类型(ReportError)携带错误码和结构化信息,适合在 API 边界序列化为 JSON 返回给客户端。

1.8 常见错误处理反模式

反模式 示例 问题 改进方案
静默吞没 result, _ := doSomething() 丢失错误信息,导致后续行为不可预测 始终检查并处理错误,至少记录日志
字符串比较 err.Error() == "not found" 脆弱,错误字符串变更会导致判断失效 使用哨兵错误或 errors.Is
过度包装 每层都使用 fmt.Errorf 包装 错误链过长,影响性能且难以阅读 仅在边界层包装,内部层直接返回
panic 代替 error panic(err) 处理业务错误 破坏控制流,可能导致程序崩溃 只有不可恢复的错误才使用 panic