错误包装

在 Go 语言中,错误包装(Error Wrapping)是从 Go 1.13 开始引入的重要特性。它允许你为错误添加上下文信息,同时保留原始错误,形成错误链。以下是错误包装的全面指南:

1. 基础包装方法

使用 fmt.Errorf + %w 动词

func readConfig() error {
    _, err := os.Open("config.json")
    if err != nil {
        return fmt.Errorf("配置文件读取失败: %w", err)
    }
    return nil
}

func main() {
    err := readConfig()
    if err != nil {
        fmt.Println(err) // 输出: 配置文件读取失败: open config.json: no such file or directory
    }
}

2. 多层错误包装

func initSystem() error {
    if err := readConfig(); err != nil {
        return fmt.Errorf("系统初始化失败: %w", err)
    }
    return nil
}

func main() {
    if err := initSystem(); err != nil {
        fmt.Println(err) 
        // 输出: 系统初始化失败: 配置文件读取失败: open config.json: no such file or directory
    }
}

3. 自定义错误类型包装

type ServiceError struct {
    ServiceName string
    Err         error
}

func (e ServiceError) Error() string {
    return fmt.Sprintf("服务 %s 错误: %v", e.ServiceName, e.Err)
}

func (e ServiceError) Unwrap() error {
    return e.Err
}

func callAPI() error {
    return ServiceError{
        ServiceName: "UserAPI",
        Err:         errors.New("连接超时"),
    }
}

func main() {
    if err := callAPI(); err != nil {
        fmt.Println(err) // 输出: 服务 UserAPI 错误: 连接超时
    }
}

4. 包装多个错误

Go 1.20+ 开始支持:

func processMultiple() error {
    var err1 = errors.New("错误1")
    var err2 = errors.New("错误2")
    return errors.Join(err1, err2)
}

func main() {
    if err := processMultiple(); err != nil {
        fmt.Println(err) // 错误1\n错误2
        // 处理多个错误
    }
}

5. 处理被包装的错误

(1) 使用 errors.Is 检查特定错误

if err := initSystem(); err != nil {
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("文件不存在错误")
    } else {
        fmt.Println("其他错误")
    }
}

(2) 使用 errors.As 提取特定错误类型

if err := callAPI(); err != nil {
    var apiErr ServiceError
    if errors.As(err, &apiErr) {
        fmt.Printf("服务名: %s, 原始错误: %v\n", 
            apiErr.ServiceName, apiErr.Err)
    }
}

(3) 获取完整错误链

func PrintErrorChain(err error) {
    fmt.Println("完整错误链:")
    for e := err; e != nil; e = errors.Unwrap(e) {
        fmt.Printf("- %s\n", e.Error())
    }
}

// 调用示例
PrintErrorChain(initSystem())
// 输出:
// 完整错误链:
// - 系统初始化失败: 配置文件读取失败: open config.json: no such file or directory
// - 配置文件读取失败: open config.json: no such file or directory
// - open config.json: no such file or directory

6. 高级包装模式

添加堆栈跟踪 (使用第三方库)

import "github.com/pkg/errors"

func dbQuery() error {
    _, err := sql.Exec("SELECT...")
    if err != nil {
        return errors.Wrap(err, "数据库查询失败")
    }
    return nil
}

func main() {
    if err := dbQuery(); err != nil {
        fmt.Printf("%+v\n", err) // 显示带堆栈的错误信息
    }
}

条件性包装

func Validate(data []byte) error {
    if len(data) == 0 {
        return errors.New("空数据")
    }
    
    var decoded Data
    if err := json.Unmarshal(data, &decoded); err != nil {
        // 仅包装解析错误
        return fmt.Errorf("JSON解析错误: %w", err)
    }
    
    if decoded.Value < 0 {
        // 直接返回不包装
        return errors.New("负值不允许")
    }
    return nil
}

7. 错误包装最佳实践

​场景​

​推荐做法​

​示例​

添加上下文信息

fmt.Errorf("上下文: %w", err)

fmt.Errorf("连接数据库失败: %w", err)

包装第三方错误

仅包装已知需要处理的错误

对于io.EOF可能不需要包装

创建新错误

使用errors.New

errors.New("无效输入")

需要携带额外数据

自定义错误类型实现Unwrap()

包含错误码、时间戳等

多重包装

Go 1.20+ 使用errors.Join

合并多个同级错误

错误传递

每层添加有意义的上下文

DB层服务层API层

8. 重要注意事项

  1. ​不要过度包装​

    // 反模式:多层不必要的包装
    return fmt.Errorf("wrapper1: %w", 
           fmt.Errorf("wrapper2: %w", 
           fmt.Errorf("wrapper3: %w", originalErr)))
  2. ​保持错误信息简洁有用​

    // 好的做法
    if err := ioutil.ReadFile(f); err != nil {
        return fmt.Errorf("读取配置文件 %s 失败: %w", f, err)
    }
  3. ​处理被包装错误的能力​

    // 被调用方可能返回包装错误
    if err := process(); err != nil {
        if errors.Is(err, SomeErrorType) {
            // 处理特定错误
        }
    }

完整包装流程图

程序执行路径                    错误包装路径
    |                              |
    v                              v
读取文件 (失败) ---------> "文件读取失败: %w" 
    |                              |
    v                              v
调用服务 --------> "服务调用失败: %w (包含文件错误)"
    |                              |
    v                              v
API处理 ---------> "API错误: %w (包含服务错误)"
    |                              |
    v                              v
主函数:使用 errors.Is/As 提取底层错误

通过合理使用错误包装,可以:

  1. 保留原始错误信息

  2. 添加上下文信息

  3. 创建清晰的错误处理路径

  4. 支持精确的错误检测

  5. 构建可维护的错误处理体系

始终考虑错误处理是业务逻辑的一部分,错误包装应使调试和维护更容易,而不是更复杂。

最后更新于