哨兵错误是预定义的全局 error 变量,必须用 == 精确比较;不可调用、不可嵌套、不可导出为小写或定义在函数内;应按领域收敛命名并避免随意包装。

哨兵错误变量不是函数,不能“调用”
Go 里没有“哨兵错误函数”这种东西——errors.New 或 fmt.Errorf 返回的是 error 类型值,而真正的哨兵错误(如 io.EOF)是预先定义好的全局变量,类型为 error,本质是地址可比较的指针或结构体实例。你不能像调用函数那样写 io.EOF(),那会直接编译失败:cannot call non-function io.EOF (type error)。
常见误解是把 var ErrNotFound = errors.New("not found") 当成可调用对象,其实它只是个变量。它的作用是被 复用 和 精确比较,不是被“调用”。
判断哨兵错误必须用 ==,不能用 errors.Is
哨兵错误的核心价值在于地址一致性:同一包内多次引用 ErrNotFound 指向同一个底层值。因此判断必须用 ==,这是唯一能保证语义正确的做法。
-
err == pkg.ErrNotFound✅ 正确、高效、意图明确 -
errors.Is(err, pkg.ErrNotFound)❌ 不必要且可能掩盖问题:它会触发错误链遍历,而哨兵错误本就不该嵌套;若有人误用fmt.Errorf("wrap: %w", pkg.ErrNotFound),errors.Is会返回 true,但已失去哨兵的精确性 -
errors.As(err, &target)❌ 完全不适用:哨兵错误没有字段可提取,也不需要类型断言
自定义哨兵错误要放在包顶层,且必须导出
如果定义在函数内部或未导出(小写开头),外部包无法访问,也就失去了“共享错误标识”的意义。
正确写法示例:
package user
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrInvalidAge = errors.New("age must be positive")
)
错误写法:
-
func do() error { return errors.New("not found") }→ 每次返回新实例,==永远 false -
var errNotFound = errors.New("...")(小写)→ 外部包无法引用,无法统一判断 - 在
init()中动态构造 → 地址不可预测,破坏哨兵语义
多个哨兵错误共存时,避免命名冲突和语义重叠
一个包里定义太多 ErrXXX 变量容易混淆,尤其当错误含义接近(比如 ErrNotFound 和 ErrNotExists)时,调用方难以选择,也增加维护成本。
建议原则:
- 按领域收敛:数据库层用
db.ErrNoRows,HTTP 层用http.ErrAbortHandler,不要全塞进一个包 - 命名体现责任边界:
json.ErrSyntax是解析失败,os.ErrNotExist是文件系统不存在,二者不可互换 - 慎用 “通用哨兵”:像
ErrUnknown或ErrInternal很难被下游精准处理,往往意味着错误分类没做好
真正难的不是定义几个变量,而是让每个哨兵错误在调用链中保持身份不变——一旦被 %w 包裹,它就不再是哨兵了。所以别在中间层随意包装,除非你明确放弃精确比较。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











