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

在 Go 语言中,错误处理是程序健壮性的基石。Go 没有传统面向对象语言中的异常捕获机制(try-catch),而是采用显式的错误返回值设计。本章将系统讲解 Go 错误处理的核心概念:error 接口、标准库提供的错误构造方法以及自定义错误类型的实现。

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

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

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

该接口仅包含一个 Error() 方法,返回描述错误信息的字符串。这意味着:

  • 任何类型只要实现了 Error() string 方法,就自动满足 error 接口。
  • 函数通过返回 error 类型来传递错误,调用方必须显式检查。
  • nil 表示没有错误,非 nil 表示存在错误。
关键原则:Go 中的错误是值,可以像其他值一样赋值、比较、传递。错误处理是代码逻辑的一部分,而非异常路径。

2.2 errors.New:快速构造简单错误

errors.New 是标准库 errors 包提供的函数,用于创建一个包含固定错误消息的 error 值。其内部实现是一个私有类型 errorString,仅存储字符串并实现 Error() 方法。

package errors

// errorString 是 errors.New 的内部实现类型
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

// New 返回一个包含给定文本的错误
func New(text string) error {
    return &errorString{text}
}

使用示例:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("发生错误:", err)
        return
    }
    fmt.Println("结果:", result)
}
适用场景:errors.New 适用于静态、固定的错误消息,不需要动态格式化参数。每次调用都会分配一个新的错误实例,但通常开销可忽略。

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

当错误消息需要包含动态信息(如变量值、上下文参数)时,fmt.Errorf 是更便捷的选择。它支持 fmt.Sprintf 的所有格式化动词,并返回 error 类型。

package main

import (
    "fmt"
)

func openFile(path string) error {
    // 模拟文件打开失败
    return fmt.Errorf("无法打开文件 %s: 权限不足", path)
}

func main() {
    err := openFile("/etc/shadow")
    if err != nil {
        fmt.Println(err)
        // 输出: 无法打开文件 /etc/shadow: 权限不足
    }
}

与 errors.New 的对比:

特性 errors.New fmt.Errorf
参数 固定字符串 格式化字符串 + 可变参数
动态内容 不支持 支持 %s、%d、%v 等
性能 略快(无格式化开销) 稍慢(需解析格式化模板)
适用场景 常量错误消息 需要嵌入运行时值的错误
注意:Go 1.13 之后,fmt.Errorf 支持 %w 动词用于错误包装(error wrapping),这将在后续章节详细讲解。在基础阶段,请先掌握 %s、%v 等常规格式化用法。

2.4 自定义错误类型:携带更多上下文

当错误需要包含结构化信息(如错误码、错误层级、内部原因)时,自定义错误类型是更好的选择。通过实现 error 接口,可以定义任意复杂的错误结构。

2.4.1 基础自定义错误

package main

import "fmt"

// ValidationError 自定义验证错误类型
type ValidationError struct {
    Field   string // 出错的字段名
    Value   interface{} // 导致错误的输入值
    Message string // 错误描述
}

// Error 实现 error 接口
func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 验证失败: %s (输入值: %v)",
        e.Field, e.Message, e.Value)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "年龄必须在 0-150 之间",
        }
    }
    return nil
}

func main() {
    err := validateAge(-5)
    if err != nil {
        // 类型断言获取具体错误信息
        if ve, ok := err.(*ValidationError); ok {
            fmt.Printf("错误字段: %s\n", ve.Field)
            fmt.Printf("错误值: %v\n", ve.Value)
        }
        fmt.Println(err)
    }
}

2.4.2 带错误码的自定义错误

在大型项目中,错误码是错误分类和国际化的重要工具。以下是一个带错误码的自定义错误实现:

package main

import "fmt"

// ErrorCode 定义错误码类型
type ErrorCode int

const (
    ErrNotFound     ErrorCode = 1001
    ErrPermission   ErrorCode = 1002
    ErrInvalidInput ErrorCode = 1003
)

// AppError 带错误码的应用错误
type AppError struct {
    Code    ErrorCode
    Message string
    Err     error // 内部原始错误(可选)
}

func (e *AppError) 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)
}

// Unwrap 支持 errors.Unwrap 链式解包
func (e *AppError) Unwrap() error {
    return e.Err
}

func getUser(id int) error {
    return &AppError{
        Code:    ErrNotFound,
        Message: "用户不存在",
        Err:     fmt.Errorf("用户 ID %d 未找到", id),
    }
}

func main() {
    err := getUser(42)
    if err != nil {
        fmt.Println(err)
        // 输出: [1001] 用户不存在: 用户 ID 42 未找到
    }
}
最佳实践:自定义错误类型应至少包含 Error() string 方法。如果需要支持错误链(error wrapping),应同时实现 Unwrap() error 方法。错误码建议使用常量定义,避免魔法数字。

2.5 错误处理核心流程

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

Go 错误处理核心流程 Go 错误处理核心流程 错误构造方式 errors.New("固定消息") fmt.Errorf("格式 %s", val) 自定义错误类型 函数返回 error return err, nil return nil, err 调用方处理 if err != nil 类型断言获取详情 日志 / 返回上层 核心原则:错误是值,显式检查,绝不忽略。 自定义错误类型可携带错误码、字段名等结构化信息,便于调用方精确处理。 错误码设计示例 ErrNotFound (1001) ErrPermission (1002) ErrInvalidInput (1003) ErrTimeout (1004) 错误码应分层设计:应用层错误码(1xxx)、系统层错误码(2xxx)、第三方错误码(3xxx) 每个错误码对应唯一的错误场景,便于监控和告警聚合

2.6 三种错误构造方式的选择策略

在实际开发中,应根据错误的使用场景选择合适的构造方式:

  1. 静态错误消息:使用 errors.New,例如 errors.New("EOF")。
  2. 需要格式化动态值:使用 fmt.Errorf,例如 fmt.Errorf("用户 %s 不存在", username)。
  3. 需要结构化信息或错误码:定义自定义错误类型,实现 error 接口。
  4. 需要错误链(wrapping):使用 fmt.Errorf 的 %w 动词或自定义 Unwrap() 方法(后续章节详解)。
实战建议:在项目初期就定义统一的错误码体系和自定义错误类型,避免后期大规模重构。对于库代码,优先使用 errors.New 或 fmt.Errorf 返回简单错误;对于业务代码,推荐使用带错误码的自定义错误类型。

2.7 错误检查的常见模式

Go 中检查错误的惯用模式包括:

模式 代码示例 说明
基本检查 if err != nil { return err } 最常用,直接返回错误
带日志 if err != nil { log.Printf("操作失败: %v", err); return err } 记录错误后返回
类型断言 if ve, ok := err.(*ValidationError); ok { ... } 获取自定义错误详情
错误码判断 if ae, ok := err.(*AppError); ok && ae.Code == ErrNotFound { ... } 根据错误码分支处理
忽略错误 _ = doSomething() 显式忽略(应谨慎使用)
避坑指南:永远不要使用 _ = doSomething() 忽略错误,除非你明确知道该错误不需要处理(如 strings.Writer.Write 的返回值)。忽略错误是 Go 程序中最常见的 bug 来源之一。

2.8 本章小结

本章覆盖了 Go 错误处理的基础知识:

  • error 接口:Go 错误处理的核心契约,任何实现了 Error() string 的类型都是错误。
  • errors.New:快速构造静态错误消息。
  • fmt.Errorf:构造带格式化参数的动态错误消息。
  • 自定义错误类型:通过结构体实现 error 接口,携带错误码、字段名等结构化信息。
  • 错误检查模式:基本检查、类型断言、错误码判断等惯用写法。