2. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、自定义错误类型

在 Go 语言中,错误处理是构建健壮 API 的基石。Go 采用显式错误返回机制,而非异常抛出。本章将深入剖析 Go 错误处理的核心:error 接口、标准库的构造方法以及自定义错误类型,为后续的搜索错误码断言与接口契约奠定基础。

2.1 error 接口:一切错误的源头

Go 语言的内建 error 接口是所有错误类型的统一抽象。它只定义了一个方法:Error() string,用于返回错误的描述信息。

// 内建 error 接口定义
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型,都自动满足 error 接口。这意味着你可以为任何自定义类型附加错误信息,使其成为合法的错误值。

关键理解: error 是一个接口,而非具体类型。函数返回 error 时,实际返回的是实现了该接口的具体实例。判断错误是否为 nil 时,需要同时检查接口的动态类型和动态值。

2.2 errors.New:最简单的错误构造

errors.New 函数接收一个字符串,返回一个包含该字符串的 error 值。它适用于构造静态、无上下文的简单错误。

import "errors"

var ErrNotFound = errors.New("resource not found")
var ErrInvalidInput = errors.New("invalid input parameter")

func FindUser(id int) (User, error) {
    if id <= 0 {
        return User{}, ErrInvalidInput
    }
    // ... 查找逻辑
    return User{}, ErrNotFound
}
注意: 使用 errors.New 构造的错误,其 Error() 返回的字符串是固定的。调用者通常只能通过字符串比较(不推荐)或 == 运算符与预定义的错误变量进行比较。字符串比较脆弱,推荐使用预定义错误变量进行相等性判断。

2.3 fmt.Errorf:带格式化上下文的错误

当需要将动态值嵌入错误消息时,fmt.Errorf 是首选。它支持 fmt 包的格式化动词,能够生成包含具体参数的错误描述。

import "fmt"

func GetUserByID(id int) (User, error) {
    if id <= 0 {
        return User{}, fmt.Errorf("invalid user ID: %d", id)
    }
    // ... 数据库查询
    if err != nil {
        return User{}, fmt.Errorf("query user by id %d failed: %w", id, err)
    }
    return user, nil
}

从 Go 1.13 开始,fmt.Errorf 支持 %w 动词,用于创建包装错误(wrapped error)。包装错误保留了原始错误的引用,允许调用者通过 errors.Is 和 errors.As 进行错误链的遍历与类型断言。

构造方式 动态内容 错误包装 适用场景
errors.New 不支持 不支持 静态、预定义的错误常量
fmt.Errorf (无 %w) 支持 不支持 带动态参数的描述性错误
fmt.Errorf (含 %w) 支持 支持 需要保留原始错误链的场景

2.4 自定义错误类型:携带结构化数据

当错误需要包含多个字段、错误码、上下文或支持特定行为时,自定义错误类型是更强大的选择。通过实现 error 接口,你可以定义自己的错误结构体。

// 自定义错误类型:包含错误码和内部错误
type AppError struct {
    Code    int
    Message string
    Err     error  // 内部原始错误
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("code=%d, message=%s, internal=%v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}

// 可选:实现 Unwrap 方法,支持错误链
func (e *AppError) Unwrap() error {
    return e.Err
}

自定义错误类型的优势在于:

  • 结构化数据: 可以携带错误码、HTTP 状态码、时间戳等字段。
  • 类型断言: 调用者可以通过 errors.As 将错误转换为具体类型,提取结构化信息。
  • 行为扩展: 可以添加额外方法,如 Temporary() bool 或 Retryable() bool。
  • 错误链支持: 通过实现 Unwrap() error 方法,使自定义错误参与错误链遍历。
最佳实践: 在 API 边界(如 HTTP 处理层、gRPC 拦截器)使用自定义错误类型,将内部错误码、业务状态码与错误消息统一封装,便于客户端根据错误码进行断言和重试策略。

2.5 错误处理核心流程

下图展示了 Go 错误处理的核心流程,从错误构造到调用者处理:

Go 错误处理核心流程 错误构造 errors.New / fmt.Errorf 自定义错误类型 AppError / 业务错误 函数返回 error return err / return nil 调用者处理 if err != nil { ... } errors.Is / errors.As / 类型断言 错误匹配 == / errors.Is 错误提取 errors.As / 类型断言 日志/返回 记录 + 向上传递 说明:错误构造后通过函数返回,调用者根据错误类型进行匹配、提取或传递

2.6 三种错误构造方式对比

下表从多个维度对比了三种错误构造方式,帮助你在不同场景下做出选择:

特性 errors.New fmt.Errorf 自定义类型
动态消息 不支持 支持 支持
错误包装 不支持 支持 (%w) 通过 Unwrap()
结构化数据 无 无 可包含任意字段
类型断言 仅 == 比较 仅 == 比较 errors.As / 类型断言
性能开销 低 中(格式化) 中(结构体分配)
适用场景 预定义错误常量 带参数的描述性错误 需要错误码、重试标志等

2.7 错误断言与接口契约的关联

在《Go 搜索错误码断言接口契约》课程中,错误处理基础是后续章节的基石。自定义错误类型携带的错误码,将成为搜索断言的核心依据。通过 errors.As 将通用 error 转换为携带错误码的具体类型,客户端可以精确判断错误性质并采取相应策略。

课程衔接: 第 3 章将介绍 errors.Is 和 errors.As 的深入用法;第 4 章开始构建搜索错误码体系。掌握本章的自定义错误类型,是后续实现错误码断言接口契约的前提。