[AI生成] go为什么推荐使用errors.Is
案例
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 模拟:打开不存在的文件,产生原始错误 os.ErrNotExist
err := openFile("not-exist.txt")
// 1. 传统 == 判等:结果是 false(错误被包装了)
fmt.Println(err == os.ErrNotExist) // 输出:false
// 2. errors.Is 判等:结果是 true(递归解包找到原始错误)
fmt.Println(errors.Is(err, os.ErrNotExist)) // 输出:true
}
// 打开文件,包装原始错误返回
func openFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 🔴 错误包装:给原始错误添加上下文
return fmt.Errorf("打开文件失败: %w", err)
}
return nil
}

第一步:看最简源码(去掉所有冗余,只留核心逻辑)
func Is(err, target error) bool {
// 1. 特殊情况:目标是nil,直接判断err是不是nil
if target == nil {
return err == nil
}
// 2. 无限循环:不停剥壳、判断
for {
// 第一步判断:如果当前错误有自定义匹配规则 → 用自定义规则
if 当前错误实现了Is方法 {
if 自定义匹配成功 {
return true
}
}
// 第二步判断:直接用 == 比较当前错误
if 当前错误 == 目标错误 {
return true
}
// 都不匹配 → 剥一层壳,拿到里面的错误
err = 解包一层(err)
// 剥到底了(没有壳了)→ 没找到,返回false
if err == nil {
return false
}
}
}
第二步:逐行大白话翻译(每一句为什么存在?干什么?)
// 函数作用:判断 err 这个错误链里,有没有 target 这个错误
func Is(err, target error) bool {
// --------------------------
// 行1:处理极端情况:目标是nil
// --------------------------
if target == nil {
return err == target
}
// 解释:
// target是nil = 你要找“无错误”
// 只有err本身是nil,才返回true,这是固定规则
// --------------------------
// 行2:判断目标能不能用 == 比较(99%场景都为true)
// --------------------------
isComparable := reflect.TypeOf(target).Comparable()
// 解释:
// 比如 os.ErrNotExist、自定义错误变量,都能用 == 比较
// 只有极少数奇怪的错误(带切片、map)不能用 ==,这里做个标记
// --------------------------
// 行3:无限循环!核心!不停剥壳+判断
// --------------------------
for {
// --------------------------
// 第一优先级:用自定义的匹配规则
// --------------------------
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 大白话解释:
// 检查当前这个错误,有没有自带「我是不是你要找的错误」的方法
// 如果有 → 直接用它自己的规则判断,匹配就返回true
// 这是给高级自定义错误留的扩展口
// --------------------------
// 第二优先级:直接用 == 比较(最常用)
// --------------------------
if isComparable && err == target {
return true
}
// 大白话解释:
// 目标能比较 + 当前错误 == 目标错误 → 找到啦!返回true
// --------------------------
// 都没匹配 → 剥一层壳!
// --------------------------
err = Unwrap(err)
// 解释:
// Unwrap 就是干一件事:把当前错误的壳剥开,返回里面的错误
// 没有壳 → 返回nil
// --------------------------
// 剥到底了 → 没找到,结束
// --------------------------
if err == nil {
return false
}
}
}
func Unwrap(err error) error {
// 检查错误有没有「剥壳」的方法
// 有 → 剥一层,返回里面的错误
// 没有 → 返回nil
u, ok := err.(interface{ Unwrap() error })
if !ok {
return nil
}
return u.Unwrap()
}
第三步:手动模拟源码执行(最透彻!一步一步走)
// 原始错误:文件不存在
原始错误 = os.ErrNotExist
// 包装错误:套一层壳
包装错误 = fmt.Errorf("打开文件失败: %w", 原始错误)
// 调用Is
errors.Is(包装错误, os.ErrNotExist)
源码循环执行全过程(肉眼可见)
第一次循环:
当前错误 = 包装错误(快递盒)
检查自定义 Is 方法 → 没有
用 == 比较:包装错误 == os.ErrNotExist?→ false
剥壳:Unwrap(包装错误) → 得到 原始错误
原始错误不是 nil → 继续循环
第二次循环:
当前错误 = 原始错误(商品)
检查自定义 Is 方法 → 没有
用 == 比较:原始错误 == os.ErrNotExist?→ true
直接返回 true,结束!
第四步:源码里的 2 个隐藏细节(彻底通透)
自定义 Is 方法优先级最高
如果你自己写的错误实现了 Is(error) bool 方法,errors.Is 会直接用你的逻辑,跳过 == 判断,扩展性拉满。
只剥一层,循环剥到底
Unwrap 每次只剥一层壳,Is 用 for 循环实现无限剥壳,直到错误为 nil。
浙公网安备 33010602011771号