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

在 Go 语言中,错误处理不是通过异常(try-catch)机制实现的,而是通过显式返回 error 值来传递错误状态。这种设计哲学要求开发者将错误视为普通值,在每个可能出错的调用点主动检查并处理。本章将系统讲解 Go 错误处理的核心基础:error 接口、标准库提供的错误构造方法以及自定义错误类型。

2.1 error 接口:一切错误的契约

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

// Go 标准库定义
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都自动满足 error 接口,这体现了 Go 语言“隐式接口”的设计理念。函数或方法通过返回 error 类型来告知调用方操作是否成功。

核心要点:在 Go 中,nil 表示没有错误,非 nil 的 error 值表示发生了错误。调用方必须检查返回值中的 error 是否为 nil。

2.2 构造简单错误:errors.New 与 fmt.Errorf

标准库提供了两种最常用的方式来创建简单的错误值:errors.New 和 fmt.Errorf。

函数 签名 特点 适用场景
errors.New func New(text string) error 仅接受静态字符串,返回一个包含该文本的 error 值 固定错误消息,无需格式化
fmt.Errorf func Errorf(format string, a ...any) error 支持 fmt.Printf 风格的格式化占位符,返回格式化后的 error 值 需要动态拼接错误信息,如包含变量值
// 使用 errors.New
var ErrNotFound = errors.New("record not found")

// 使用 fmt.Errorf
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero: cannot divide %d by %d", a, b)
    }
    return a / b, nil
}
最佳实践:对于包级别可导出的哨兵错误(Sentinel Error),优先使用 errors.New 并声明为 var 变量,方便调用方使用 == 或 errors.Is 进行断言。

2.3 自定义 error 类型:携带更多上下文

当错误需要携带结构化信息(如错误码、内部原因、堆栈信息)时,自定义 error 类型是更好的选择。通过实现 Error() string 方法,任何结构体都可以成为 error 类型。

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

// 实现 error 接口
func (e *MyError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("code=%d, message=%s, cause=%v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}

// 使用示例
func Process(id int) error {
    if id < 0 {
        return &MyError{
            Code:    400,
            Message: "invalid id",
            Err:     fmt.Errorf("id must be non-negative, got %d", id),
        }
    }
    // ... 正常处理
    return nil
}
注意:自定义错误类型通常使用指针接收者实现 Error() 方法,以确保接口断言时类型匹配。同时,自定义错误类型应提供 Unwrap() 方法以支持错误链(将在后续章节详述)。

2.4 三种错误构造方式对比

下表从多个维度对比了三种错误构造方式的差异,帮助你在实际开发中做出合适的选择。

维度 errors.New fmt.Errorf 自定义 error 类型
信息丰富度 仅静态文本 支持格式化 完全可控,可包含任意字段
类型断言 无法断言(无类型) 无法断言(无类型) 支持 errors.As 断言
错误链支持 不支持 Go 1.13+ 支持 %w 需手动实现 Unwrap()
性能开销 低 中(格式化开销) 中到高(结构体分配)
适用场景 哨兵错误、常量错误 快速构造带变量的错误 需要错误码、分类、堆栈等结构化信息

2.5 错误处理基础流程

下面的 SVG 图展示了 Go 程序中典型的错误处理流程:从函数返回错误,到调用方检查并做出相应处理。

Go 错误处理基础流程 调用方 函数执行 err != nil ? 错误处理 正常流程 错误处理方式 • 记录日志并返回 • 包装后向上传递 正常流程 继续后续操作

2.6 实战:构建一个带错误码的自定义错误

以下示例展示了一个完整的自定义错误类型实现,包含错误码、用户消息和内部原因,并提供了 Unwrap() 方法以支持错误链。

package main

import (
    "errors"
    "fmt"
)

// AppError 应用级错误,包含错误码和内部原因
type AppError struct {
    Code    int
    Message string
    Err     error
}

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

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

// 预定义错误码
const (
    ErrCodeNotFound   = 404
    ErrCodeInternal   = 500
    ErrCodeValidation = 400
)

// 哨兵错误
var ErrNotFound = errors.New("resource not found")

// GetUser 模拟获取用户信息
func GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, &AppError{
            Code:    ErrCodeValidation,
            Message: "invalid user id",
            Err:     fmt.Errorf("id must be positive, got %d", id),
        }
    }
    if id > 100 {
        return nil, &AppError{
            Code:    ErrCodeNotFound,
            Message: "user not found",
            Err:     ErrNotFound,
        }
    }
    return &User{ID: id, Name: "Alice"}, nil
}

type User struct {
    ID   int
    Name string
}

func main() {
    _, err := GetUser(-1)
    if err != nil {
        var appErr *AppError
        if errors.As(err, &appErr) {
            fmt.Printf("错误码: %d\n", appErr.Code)
            fmt.Printf("消息: %s\n", appErr.Message)
            fmt.Printf("内部原因: %v\n", appErr.Err)
        }
    }
}
关键点:通过 errors.As 函数可以将错误断言为自定义类型,从而提取结构化字段。这是 Go 错误码断言的核心机制之一,后续章节将深入展开。

2.7 本章小结

  • error 接口是 Go 错误处理的基石,任何实现了 Error() string 方法的类型都是 error。
  • errors.New 适用于构造静态哨兵错误,fmt.Errorf 适用于快速格式化错误消息。
  • 自定义 error 类型通过结构体字段携带错误码、内部原因等结构化信息,支持 errors.As 断言。
  • 选择哪种构造方式取决于是否需要类型断言、错误链支持以及信息丰富度。