errors.is 能判断错误链中的目标错误,因为它通过递归调用 unwrap() 展开错误链,并在每层用 == 或 is() 判断是否匹配目标,支持 %w 包装和自定义 is() 方法。

errors.Is 为什么能判断错误链中的目标错误
errors.Is 不是简单比对错误值,而是沿着 Unwrap() 方法一层层向下展开错误链,对每个环节调用 == 或 Is() 判断是否匹配目标。它天然支持标准库中用 fmt.Errorf("...: %w", err) 构建的嵌套错误,也兼容实现了 interface{ Is(error) bool } 的自定义错误类型。
常见错误现象:errors.Is(err, io.EOF) 返回 false,但你明明看到日志里有 "read tcp: i/o timeout: EOF" —— 这往往是因为错误链没被正确构建(比如用了 %v 而非 %w),或中间某层错误没实现 Unwrap()。
- 必须用
%w动词包装底层错误,%s、%v会截断链 - 第三方库返回的错误如果没实现
Unwrap()(如某些老版本sql.ErrNoRows),errors.Is就无法穿透 - 自定义错误若想参与链式判断,需同时实现
Error() string和Unwrap() error(或Is(error) bool)
什么时候该用 errors.Is 而不是 errors.As 或 ==
直接用 == 只能判断是否为同一个错误实例,对包装后的错误完全失效;errors.As 是类型断言,用于提取底层错误的具体类型,比如获取 *os.PathError 做进一步字段访问;而 errors.Is 专注“语义相等”——你关心的是“是不是这个错误含义”,而不是“是不是这个对象”或“能不能转成这个类型”。
使用场景举例:HTTP handler 中统一处理 context.Canceled 或 io.EOF,不关心它们被哪层函数包装过;数据库操作中识别 sql.ErrNoRows 并转为业务上的“未找到”,也不依赖具体返回的是 *sql.Error 还是封装过的 app.ErrNotFound。
立即学习“go语言免费学习笔记(深入)”;
- 判断标准错误常量(
io.EOF、context.Canceled、os.ErrNotExist)优先用errors.Is - 需要读取底层错误字段(如
Path、Err)时,改用errors.As - 自定义错误实现了
Is()方法,且逻辑不是简单等于(例如:把多种网络超时都视为“临时失败”),errors.Is会自动调用它
errors.Is 在 Go 1.13+ 和旧版本的兼容性陷阱
Go 1.13 引入了错误链支持,errors.Is 和 errors.As 才正式可用。如果你在 Go gopkg.in/yaml.v2 这类不支持错误链的老库,它们返回的错误可能没有 Unwrap(),导致 errors.Is 在第一层就停止展开。
性能影响很小:最坏情况是遍历整个错误链,但链长通常不超过 5 层;相比反射或字符串匹配,它开销更低、语义更准。
- 检查
go version,低于 1.13 必须升级或换兼容方案(如手动递归cause()) - 用
errors.Unwrap(err)手动测试能否拿到下一层错误,确认第三方库是否适配 - 避免在 hot path(如高频 RPC 响应)中反复调用
errors.Is判断同一组错误,可提前缓存判断结果
如何写一个能被 errors.Is 正确识别的自定义错误
核心就两条:实现 Error() string,再加一个 Unwrap() error 返回底层错误(如果有),或者实现 Is(target error) bool 自定义匹配逻辑。前者更通用,后者更灵活。
示例:一个包装了重试次数的错误,希望所有重试超限都视为 “永久失败”:
type RetryError struct {
Err error
Try int
Max int
}
func (e *RetryError) Error() string {
return fmt.Sprintf("failed after %d tries: %v", e.Max, e.Err)
}
func (e *RetryError) Unwrap() error {
return e.Err
}
// 让 errors.Is(err, ErrPermanent) 也能命中
func (e *RetryError) Is(target error) bool {
if target == ErrPermanent {
return e.Try >= e.Max
}
return errors.Is(e.Err, target)
}
- 如果错误没有底层原因(纯新构造),
Unwrap()返回nil - 不要在
Is()里做耗时操作(如 IO、锁),它可能被频繁调用 - 多个包装层共存时,确保每层都正确传递
%w,否则链会在某处断裂
fmt.Printf("%+v", err) 输出里有没有 unwrapped 字样,或者直接 for err != nil { fmt.Println(reflect.TypeOf(err)); err = errors.Unwrap(err) } 看链到底有多长。










