[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
}

image

第一步:看最简源码(去掉所有冗余,只留核心逻辑)

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。

posted on 2026-05-12 08:59  王景迁  阅读(10)  评论(0)    收藏  举报

导航