不能直接用 errors.New("xxx") 定义全局错误,因为每次调用都会创建新对象,导致 == 比较失败;正确做法是用包级 var 声明一次 Sentinel 错误变量,如 var ErrNotFound = errors.New("user not found"),并统一使用 errors.Is 判断。

为什么不能直接用 errors.New("xxx") 定义全局错误
因为每次调用 errors.New 都会创建新对象,== 比较必然失败,导致下游无法可靠判断错误类型。Sentinel errors 的核心是「复用同一个错误值」,而不是复用错误消息。
- 常见错误现象:
if err == ErrNotFound返回false,即使 err 是errors.New("not found") - 正确做法:用变量声明一次,全局只初始化一次
- 不要在函数里重复
var ErrNotFound = errors.New("not found")—— 位置不对就等于没做
如何正确定义和导出 Sentinel 错误常量
Go 中没有真正的「错误常量」(const),必须用 var 声明包级变量,并确保初始化只发生一次。导出名要大写,且命名带 Err 前缀便于识别。
- 推荐写法:
var ErrNotFound = errors.New("user not found") - 避免用
fmt.Errorf初始化 —— 它返回 *fmt.wrapError,底层结构不同,==仍可能失效 - 如果需带上下文但又想保持可比较性,用
errors.Join或包装器(如fmt.Errorf("wrap: %w", ErrNotFound))后,必须用errors.Is判断,而非== - 多个相关错误建议集中定义在
errors.go文件中,不散落在各处
errors.Is 和 errors.As 在 Sentinel 场景下的真实用途
当错误被多层包装(比如中间件、重试逻辑、日志装饰)后,== 就完全不可靠了。errors.Is 才是 Sentinel errors 的实际落地接口。
-
errors.Is(err, ErrNotFound)能穿透fmt.Errorf("failed: %w", ErrNotFound)层级 -
errors.As(err, &target)主要用于自定义错误类型(如实现了Unwrap() error的 struct),对纯errors.New常量无效 - 性能影响极小:遍历包装链一般只有 1–3 层,开销可忽略
- 兼容性注意:Go 1.13+ 才支持,低于此版本需用第三方库或手动递归
Unwrap()
容易被忽略的边界情况:nil 错误、跨包引用、测试 Mock
看似简单的事,在工程中常因细节翻车。尤其当错误变量被其他包 import,或单元测试需要伪造时。
立即学习“go语言免费学习笔记(深入)”;
- 永远检查
err != nil再调用errors.Is—— 对nil调用会 panic - 跨包引用时,确保目标包已初始化:如果
pkgA的ErrNotFound依赖pkgB.init(),而pkgB又依赖pkgA,可能触发 init 循环 - 测试中不要用
errors.New("user not found")去 mockErrNotFound—— 即使消息一样,==也不成立;应直接返回原变量或用errors.Is断言 - 如果错误需要携带字段(如错误码、traceID),Sentinel 就不够用了,得切到自定义 error struct +
Is()方法
真正难的不是定义一个 var ErrXXX,而是让整个调用链从 DB 层到 HTTP handler 都统一用 errors.Is 做判断,且没人偷偷用字符串匹配或 == 硬比较。










