errors.Is 不能直接用 errors.New("xxx") 比较,因每次调用生成新实例、地址不同;必须用包级变量哨兵错误(如 ErrNotFound);标准错误如 io.EOF 实现了 Is 方法可跨包装匹配;errors.Is(err, nil) 恒为 false,判空仍需 err == nil。

errors.Is 为什么不能直接用 errors.New("xxx") 比较
因为 errors.New("xxx") 每次调用都生成新实例,地址不同,errors.Is 内部靠指针相等或 Is() 方法判断,临时构造的错误永远不匹配。
必须用包级变量:比如 var ErrNotFound = errors.New("not found"),再传入 errors.Is(err, ErrNotFound)。
-
errors.Is(err, errors.New("not found"))永远返回false -
io.EOF、os.ErrNotExist这类标准哨兵错误本身实现了Is(error) bool,所以能跨包装层匹配 -
errors.Is(err, nil)总是false;判空仍得用err == nil
errors.As 的第二个参数为什么必须是指针
errors.As 要把匹配到的错误值“写进去”,不是返回新值。它需要一个可寻址的位置来赋值,所以目标变量必须是指针类型(如 *os.PathError),且传的是它的地址(&pathErr)。
常见错误:errors.As(err, pathErr)(传值)或 errors.As(err, &err)(err 是 error 接口,&err 类型不匹配)都会编译失败。
立即学习“go语言免费学习笔记(深入)”;
- 正确写法:
var pathErr *os.PathError; errors.As(err, &pathErr) - 匹配失败时,
pathErr保持原值(不会被置零),必须检查返回值再使用 - 支持接口类型提取,但前提是该接口实现了
As(interface{}) bool方法
为什么 fmt.Errorf("%v", err) 会让 errors.Is 和 errors.As 失效
只有 %w 才会建立可解包的错误链;%v、%s、字符串拼接或 errors.New(... + err.Error()) 都把原始错误转成字符串,彻底丢失类型和值,Unwrap() 返回 nil,链在此断裂。
哪怕你只在日志里用了一次 fmt.Errorf("log: %v", err),再把这个错误传给上层,errors.Is 就再也找不到 io.EOF 了。
- ✅ 正确:
fmt.Errorf("read failed: %w", io.EOF) - ❌ 断链:
fmt.Errorf("read failed: %v", io.EOF)或fmt.Errorf("read failed: %w, retry=%d", err, n)(%w不在末尾,Go 编译器直接报错) - 第三方库返回的错误若没用
%w包装,你需要手动再包一次:return fmt.Errorf("api call failed: %w", apiErr)
自定义错误类型如何让 errors.Is 和 errors.As 生效
标准库的 fmt.Errorf 和 errors.Join 自带 Unwrap(),但你自己的结构体错误默认没有。如果不实现,errors.Is 和 errors.As 就完全看不到它包裹的内容。
核心就两点:在字段里存下被包裹的错误,并在 Unwrap() 方法中返回它。
- 如果错误没包裹其他错误,
Unwrap()应返回nil(合法且规范) - 想参与
errors.Is判定,可选实现Is(error) bool方法(比如转发给底层错误) - 想被
errors.As提取,类型本身就得能被安全赋值给目标指针,通常要求接收者是指针类型
最易被忽略的点:错误链里只要有一层没实现 Unwrap() 或用了 %v,整条链就断了——errors.Is 和 errors.As 不会跳过它继续往下查,而是直接终止遍历。


















