错误包装与检查
本教程共 80 篇 · 第 52 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
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
下一章讲哨兵错误和自定义错误类型。