2. error 接口:定义、实现与基础用法

Go 语言的错误处理核心是一个极其简洁的内建接口——error。理解它的定义、如何实现自定义错误,以及 errors.New 与 fmt.Errorf 的底层机制,是掌握错误码断言的前提。

2.1 error 接口定义

Go 源码中 error 的定义只有一行:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都自动满足 error 接口。这意味着你可以用任意结构体、指针类型甚至基本类型来承载错误信息。

特性 说明
接口方法数 1 个:Error() string
返回值 人类可读的错误描述字符串
实现方式 隐式实现,无需 implements 关键字
零值 nil,表示无错误
关键认知: error 接口只负责“描述错误”,不携带错误码、堆栈或类型信息。这正是后续章节引入错误码断言的原因——我们需要在 error 之上附加结构化数据。

2.2 实现自定义错误

通过定义结构体并实现 Error() 方法,你可以创建携带额外上下文的错误类型。

// 自定义错误类型:携带错误码和内部原因
type ReportError struct {
    Code    int
    Message string
    Err     error  // 包装原始错误
}

func (e *ReportError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// 使用示例
func parseReport(data []byte) error {
    if len(data) == 0 {
        return &ReportError{
            Code:    1001,
            Message: "empty report data",
            Err:     nil,
        }
    }
    // 正常处理...
    return nil
}

自定义错误的优势在于:

  • 携带结构化字段:错误码、严重级别、时间戳等
  • 支持错误链:通过 Err 字段保留原始错误
  • 可被类型断言识别:调用方可以用 errors.As 提取具体类型
注意: 自定义错误类型通常使用指针接收者(*ReportError),因为错误往往需要被共享和比较。值接收者可能导致接口比较时的意外行为。

2.3 errors.New 与 fmt.Errorf 基础用法

标准库提供了两个最常用的错误构造器:

函数 签名 特点 适用场景
errors.New func New(text string) error 固定字符串,无格式化 静态错误消息
fmt.Errorf func Errorf(format string, a ...any) error 支持 %s、%d、%v 等格式化 动态拼接错误信息
// errors.New:纯静态错误
var ErrNotFound = errors.New("report not found")

// fmt.Errorf:动态格式化
func validateReportID(id int) error {
    if id <= 0 {
        return fmt.Errorf("invalid report ID: %d", id)
    }
    return nil
}

// 组合使用:包装上下文
func loadReport(id int) error {
    if id <= 0 {
        return fmt.Errorf("load failed: %w", ErrNotFound)
    }
    return nil
}
最佳实践: 在 Go 1.13+ 中,fmt.Errorf 支持 %w 动词来包装错误,生成可被 errors.Is 和 errors.As 解包的错误链。这是构建错误码断言体系的基础设施。

2.4 三种错误构造方式对比

下面的 SVG 图展示了 errors.New、fmt.Errorf 和自定义错误类型在内存结构上的差异:

三种错误构造方式对比 errors.New("msg") errorString 结构体 s: "msg" (固定字符串) 不可解包,无结构化字段 fmt.Errorf("fmt %d", n) wrapError 结构体 msg: "fmt 42" (格式化后) %w 支持错误链解包 自定义 ReportError ReportError 结构体 Code: 1001, Message: "..." Err: 原始错误 (可嵌套) 统一 error 接口:Error() string 调用方:类型断言 / errors.As 提取

2.5 错误比较与断言基础

在进入错误码断言之前,先掌握两种基本的错误比较方式:

// 1. 值比较:使用 errors.Is 检查错误链中是否包含特定哨兵错误
if errors.Is(err, ErrNotFound) {
    // 处理“未找到”逻辑
}

// 2. 类型比较:使用 errors.As 提取特定类型的错误实例
var reportErr *ReportError
if errors.As(err, &reportErr) {
    fmt.Printf("错误码: %d, 消息: %s\n", reportErr.Code, reportErr.Message)
}
实战提示: 在报表系统中,推荐优先使用 errors.As 配合自定义错误类型来获取错误码,而不是依赖字符串匹配。字符串匹配脆弱且无法应对国际化场景。

2.6 本章小结

  • error 接口:仅包含 Error() string 一个方法,所有错误类型隐式实现它。
  • 自定义错误:通过结构体 + Error() 方法创建,可携带错误码、原始错误等结构化字段。
  • errors.New:构造静态错误消息,返回不可解包的 errorString。
  • fmt.Errorf:支持格式化与 %w 包装,生成可解包的错误链。
  • 比较机制:errors.Is 用于值比较,errors.As 用于类型提取,是错误码断言的前置技能。