首页 / Go 语言入门教程 / 错误包装与检查

Go 语言入门教程

错误包装与检查

本教程共 80 篇 · 第 52 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程错误处理错误包装errors.Iserrors.Aserrors.Join

52. 错误包装与检查

本节目标:学会用 %w 包装错误保留错误链,用 errors.Is/As 检查错误,用 errors.Join 合并多个错误。

为什么需要错误包装

函数调用是层层嵌套的。底层出错后,往上传递时每一层可能加自己的上下文信息。如果没有包装机制,原始错误类型会丢失:

// 没有包装:用 %v 只把错误变成字符串
func ReadConfig(path string) error {
    _, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("读取配置失败: %v", err) // 原始错误类型丢了
    }
    return nil
}

// 调用方无法用类型检查判断是不是文件不存在
err := ReadConfig("config.yaml")
// errors.Is(err, os.ErrNotExist) // false!原始错误已经变成字符串了

Go 1.13 引入了错误包装机制,用 %w 动词保留错误链:

// 有包装:用 %w 保留原始错误
func ReadConfig(path string) error {
    _, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("读取配置失败: %w", err) // 原始错误保留了
    }
    return nil
}

// 调用方可以检查原始错误类型
err := ReadConfig("config.yaml")
errors.Is(err, os.ErrNotExist) // true!能穿透包装层

%w 包装错误

%w%v 的区别:

  • %v:把错误变成字符串,原始错误类型丢失
  • %w:把错误包装起来,保留原始错误的类型和值
original := errors.New("文件不存在")

// %v 包装:变成字符串
wrapped1 := fmt.Errorf("读取失败: %v", original)
fmt.Println(errors.Is(wrapped1, original)) // false  找不到原始错误

// %w 包装:保留错误链
wrapped2 := fmt.Errorf("读取失败: %w", original)
fmt.Println(errors.Is(wrapped2, original)) // true  能找到原始错误
Note

一个 fmt.Errorf 只能用一个 %w。如果你想包装多个错误,用 errors.Join

errors.Is:判断错误是否匹配

errors.Is(err, target) 沿着错误链查找,看是否有错误等于 target:

package main

import (
    "errors"
    "fmt"
    "os"
)

func ReadFile(path string) error {
    _, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("读取 %s 失败: %w", path, err)
    }
    return nil
}

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

errors.Is 会沿着错误链一层层往下找,直到找到匹配的错误或到底。这就是包装的好处──即使错误被包了好几层,你依然能检查到最底层的错误类型。

Tip

判断错误类型时,用 errors.Is 而不是 ==== 只能匹配最外层,errors.Is 能穿透包装层。

// 错误:用 == 判断包装后的错误
if err == os.ErrNotExist { // false,因为 err 被包装过

// 正确:用 errors.Is
if errors.Is(err, os.ErrNotExist) { // true

errors.As:提取特定类型的错误

errors.As 沿着错误链查找特定类型的错误,并赋值给目标变量:

package main

import (
    "errors"
    "fmt"
    "net"
)

func Connect() error {
    _, err := net.Dial("tcp", "不存在的地址:80")
    if err != nil {
        return fmt.Errorf("连接失败: %w", err)
    }
    return nil
}

func main() {
    err := Connect()

    // 尝试提取 *net.OpError 类型
    var opErr *net.OpError
    if errors.As(err, &opErr) {
        fmt.Println("操作类型:", opErr.Op)     // dial
        fmt.Println("网络:", opErr.Net)        // tcp
        fmt.Println("地址:", opErr.Addr)       // 不存在的地址:80
    }
}

errors.As(err, &target) 的第二个参数是指向目标类型的指针。它沿错误链查找,找到第一个类型匹配的错误,赋值给 target 并返回 true。

Warning

errors.As 的第二个参数必须是指向 error 接口的指针。如果目标类型是 *MyError,传入的应该是 **MyError(指向 MyError 指针的指针)。

errors.Join:合并多个错误

Go 1.20 引入了 errors.Join,可以把多个错误合并成一个:

package main

import (
    "errors"
    "fmt"
)

func ValidateUser(name string, age int) error {
    var errs []error

    if name == "" {
        errs = append(errs, errors.New("名字不能为空"))
    }
    if age < 0 {
        errs = append(errs, errors.New("年龄不能为负数"))
    }
    if age > 150 {
        errs = append(errs, errors.New("年龄不合理"))
    }

    return errors.Join(errs...) // 合并所有错误
}

func main() {
    err := ValidateUser("", -5)
    fmt.Println(err)
    // 名字不能为空
    // 年龄不能为负数

    // 检查是否包含某个错误
    fmt.Println(errors.Is(err, errors.New("名字不能为空")))
    // 注意:这里返回 false,因为 errors.New 每次创建的是新实例
}

errors.Join 把多个 error 合并成一个。合并后的错误调用 Error() 时,会把所有子错误的描述用换行符连起来。

Note

errors.Join 是 Go 1.20(2023 年 2 月)引入的。之前要合并多个错误得自己拼接字符串或用第三方库。

错误链示意

%w 包装后,错误形成一条链:

最外层错误
  └─ 包装的错误
       └─ 更内层的错误
            └─ 最原始的错误
  • errors.Is 沿着这条链找匹配
  • errors.As 沿着这条链找类型匹配
  • errors.Unwrap 取出下一层错误(一般不需要直接用)

实际用法总结

操作函数用途
包装错误fmt.Errorf("...: %w", err)加上下文,保留原始错误
判断错误值errors.Is(err, target)判断是否是某个特定错误
提取错误类型errors.As(err, &target)获取错误的结构化信息
合并错误errors.Join(errs...)把多个错误合为一个
解包错误errors.Unwrap(err)取出被包装的下一层

小结

错误包装和检查是 Go 1.13+ 的核心特性:

  • %w 包装错误,保留错误链
  • errors.Is 判断错误是否匹配(穿透包装)
  • errors.As 提取特定类型的错误信息
  • errors.Join(Go 1.20)合并多个错误
  • 不要用 == 判断包装后的错误,用 errors.Is

下一章讲哨兵错误和自定义错误类型。