2. 基础错误类型:error 接口的定义、errors.New() 创建简单错误、fmt.Errorf() 格式化错误、哨兵错误(Sentinel Error)模式

在 Go 语言中,错误处理的核心是 error 接口。理解这个接口的定义以及如何创建和识别错误,是构建统一错误处理模式的基石。本章将深入剖析 Go 语言的基础错误类型,从最简单的错误创建到哨兵错误模式,为后续章节的业务层错误封装打下坚实基础。

2.1 error 接口的定义

Go 语言通过一个预定义的接口类型 error 来表示错误状态。这个接口极其简洁,只包含一个方法:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误值。这意味着 Go 的错误处理是类型安全的,并且非常灵活。你可以创建任意复杂的错误类型,只要它满足这个接口。

核心要点: error 是一个内置接口,而非异常机制。函数通过返回 error 值来告知调用方操作是否成功。调用方必须显式检查错误,这强制了健壮的错误处理习惯。

2.2 errors.New() 创建简单错误

标准库 errors 包提供了 New() 函数,用于创建包含静态文本的简单错误。这是最基础、最常用的错误创建方式。

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)
    } else {
        fmt.Println("结果:", result)
    }
}

特点:

  • 简单直接: 适用于不需要额外上下文信息的静态错误消息。
  • 不可变: 创建的 error 值是不可变的,其内部错误消息在创建后无法修改。
  • 性能高效: 内部实现非常轻量,不涉及格式化操作。

2.3 fmt.Errorf() 格式化错误

当需要在错误消息中嵌入动态信息(如变量值、参数等)时,fmt.Errorf() 是更合适的选择。它支持 fmt 包的所有格式化动词。

package main

import (
    "fmt"
    "os"
)

func openConfigFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // 使用 %w 动词包装原始错误(Go 1.13+)
        return fmt.Errorf("打开配置文件 %s 失败: %w", path, err)
    }
    return nil
}

func main() {
    err := openConfigFile("/etc/myapp/config.yaml")
    if err != nil {
        fmt.Println(err)
        // 输出: 打开配置文件 /etc/myapp/config.yaml 失败: open /etc/myapp/config.yaml: no such file or directory
    }
}
关键区别: fmt.Errorf() 与 errors.New() 的主要区别在于格式化能力。前者可以创建包含动态内容的错误,而后者只能创建静态错误。从 Go 1.13 开始,fmt.Errorf() 支持 %w 动词来包装错误,这是错误链的基础。

2.4 哨兵错误 (Sentinel Error) 模式

哨兵错误是预定义的、命名的错误变量,用于表示特定的、可预期的错误条件。调用方可以通过 == 运算符直接比较错误值,从而做出不同的处理决策。

定义哨兵错误:

package main

import (
    "errors"
    "fmt"
)

// 定义哨兵错误
var ErrNotFound = errors.New("资源未找到")
var ErrPermissionDenied = errors.New("权限不足")
var ErrTimeout = errors.New("操作超时")

func queryResource(id int) error {
    if id < 0 {
        return ErrNotFound
    }
    if id == 0 {
        return ErrPermissionDenied
    }
    // ... 正常处理
    return nil
}

func main() {
    err := queryResource(-1)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("处理未找到错误: 返回 404")
    } else if errors.Is(err, ErrPermissionDenied) {
        fmt.Println("处理权限错误: 返回 403")
    } else if err != nil {
        fmt.Println("其他错误:", err)
    }
}
使用建议: 哨兵错误适用于包级别或模块级别的、调用方需要明确区分的错误场景。但应避免过度使用,因为每个哨兵错误都成为包公共 API 的一部分,增加了耦合。在业务层,更推荐使用自定义错误类型来携带更多上下文。

2.5 三种错误创建方式对比

特性 errors.New() fmt.Errorf() 哨兵错误
创建方式 静态文本 格式化文本 预定义变量
动态内容 不支持 支持 不支持(静态)
错误比较 == 或 errors.Is() errors.Is()(需 %w) == 或 errors.Is()
适用场景 简单、固定的错误消息 需要嵌入变量或包装错误 可预期的、需要被调用方区分的错误
性能 高 中(涉及格式化) 高

2.6 错误处理流程与哨兵错误模式

下面的 SVG 图展示了在业务层中,如何使用哨兵错误模式进行错误判断和分支处理。调用方通过 errors.Is() 函数(或 ==)与预定义的哨兵错误进行比较,从而执行不同的错误处理逻辑。

哨兵错误模式处理流程 调用业务函数 err != nil ? 是 否 正常处理 继续执行 errors.Is() 匹配哨兵错误 哨兵错误匹配结果 ErrNotFound → 返回 404 ErrPermissionDenied → 返回 403 其他错误 → 返回 500 返回错误响应 返回成功结果

2.7 哨兵错误与自定义错误类型的对比

在业务层设计中,哨兵错误和自定义错误类型各有适用场景。下面的表格可以帮助你做出选择:

维度 哨兵错误 自定义错误类型
定义方式 var ErrX = errors.New("...") type MyError struct { ... }
携带上下文 有限(仅静态消息) 丰富(可包含字段、方法)
比较方式 == 或 errors.Is() errors.As() 类型断言
适用层级 包边界、库 API 业务层、领域层
扩展性 低(新增哨兵需改 API) 高(可继承、组合)
典型场景 io.EOF, sql.ErrNoRows 业务错误码、验证错误
最佳实践: 在业务层中,建议将哨兵错误用于包级别的、调用方必须明确处理的“契约式”错误(如“资源不存在”、“权限不足”)。对于需要携带动态数据(如字段名、期望值、实际值)的错误,应优先使用自定义错误类型,并结合 fmt.Errorf() 的 %w 动词进行包装,以保留错误链。

2.8 本章小结

  • error 接口 是 Go 错误处理的核心,只要求实现 Error() string 方法。
  • errors.New() 用于创建静态、简单的错误消息。
  • fmt.Errorf() 支持格式化错误消息,并通过 %w 动词实现错误包装。
  • 哨兵错误模式 通过预定义的错误变量,使调用方能够精确区分错误类型,是构建可预测错误处理流程的重要工具。
  • 在业务层设计中,应根据场景选择哨兵错误或自定义错误类型,并遵循“契约式”错误设计原则。