errors.is 对自定义 sentinel error 失效是因为每次 &errnotfound 生成新指针,导致地址不等;正确定义需用包级 var 变量(如 var errnotfound = errors.new("not found")),避免函数返回指针或使用 const/func() error。

Go 中 errors.Is 为什么对自定义 Sentinel Error 失效?
因为 errors.Is 默认只做值比较(或底层 ==),而你返回的是指针(比如 &ErrNotFound),每次调用都生成新地址,errors.Is(err, ErrNotFound) 必然返回 false。
常见错误现象:errors.Is(err, ErrNotFound) 总是 false,哪怕 err 确实是你自己构造的 sentinel 错误;errors.As 同样不生效,除非你显式实现了 Unwrap() 或用了 fmt.Errorf("%w", ...) 包装。
- 根本原因:Go 的 error 接口是值类型,
&ErrNotFound每次取地址都产生新指针,无法稳定比较 - 正确做法:sentinel error 必须是包级变量(即全局唯一地址),且直接导出变量名,**不要**在函数里
return &someErr - 典型反例:
func NotFound() error { return &ErrNotFound }—— 这会让每次调用返回不同指针,errors.Is完全失效
如何正确定义和使用 Sentinel Error 变量?
必须用 var 声明顶层变量,确保地址唯一;若需带上下文信息(如 ID),再额外封装结构体 + 实现 Error() 方法,但 sentinel 本身仍保持不变。
示例:
立即学习“go语言免费学习笔记(深入)”;
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)
使用时直接传值或比较:
if errors.Is(err, ErrNotFound) { ... }
- 不要用
const:Go 不允许 const 是 interface 类型,error是接口,所以const ErrNotFound = errors.New(...)编译不过 - 不要用
func() error封装:它破坏地址稳定性,errors.Is无法识别 - 如果必须动态构造(如带 ID 的 not-found),应另写一个函数返回普通 error(如
NotFoundForID(id string) error),内部用fmt.Errorf("not found for %s: %w", id, ErrNotFound)包装,让errors.Is能沿%w向上追溯
自定义 error 类型 + Sentinel 共存时的陷阱
当你既有 var ErrNotFound = errors.New("not found"),又定义了 type NotFoundError struct{ ID string } 并实现 Error(),就容易混淆比较逻辑。
常见错误现象:errors.Is(err, ErrNotFound) 对 NotFoundError 实例返回 false,即使它的字符串输出一样。
- 原因:
NotFoundError没有实现Unwrap(),errors.Is不会尝试解包,只比对当前 error 是否等于ErrNotFound(显然不是) - 修复方式:在自定义 error 类型中加
Unwrap() error方法,返回ErrNotFound(或nil表示无包装) - 注意:若返回非 nil,
errors.Is会递归检查该值,所以可安全复用 sentinel 变量
示例:
立即学习“go语言免费学习笔记(深入)”;
type NotFoundError struct{ ID string }
func (e *NotFoundError) Error() string { return "not found" }
func (e *NotFoundError) Unwrap() error { return ErrNotFound }
为什么不用 == 直接比较指针?
因为 err == ErrNotFound 看似能工作,但非常脆弱:一旦 error 被 fmt.Errorf 包装过(哪怕只是加个前缀),== 就立刻失效;而 errors.Is 支持递归解包,兼容性更好。
-
==只比较当前 error 值是否为同一内存地址,不处理包装链 -
errors.Is会逐层调用Unwrap(),直到匹配或到nil,这才是标准做法 - 性能影响几乎可忽略:现代 Go 的
errors.Is已优化为内联+快速路径,深度一般不超过 2–3 层 - 兼容性重点:Go 1.13+ 引入
%w和errors.Is/As,老版本(github.com/pkg/errors)或手动遍历Unwrap()
真正难搞的点不在定义,而在团队协作时有人随手写 return fmt.Errorf("xxx: %v", ErrNotFound) 却忘了加 %w —— 这种错误不会报错,但会让所有 errors.Is 判断静默失败。得靠 code review 或静态检查工具(如 errcheck)兜底。










