哨兵错误必须用var定义为包级导出变量,值为errors.New创建的唯一error实例,通过==比较;const和函数内errors.New均错误;应集中定义、命名规范、信息简洁且语义一致。

必须用 var 定义为包级导出变量,不能用 const 或函数内 errors.New
哨兵错误本质是内存地址唯一的 error 实例,靠 == 比较指针是否相同。而 const 声明的是字符串字面量,类型不是 error;函数里每次调用 errors.New 都会分配新对象,地址不同,== 必然失败。
-
var ErrNotFound = errors.New("user not found")✅ 正确:包初始化时创建一次,地址固定 -
const ErrNotFound = "user not found"❌ 错误:类型是string,无法参与error判断 -
func load() error { return errors.New("user not found") }❌ 错误:每次返回新实例,外部无法稳定比较 - 所有哨兵应集中定义在
errors.go(或类似统一文件),避免多文件重复定义同名变量
调用方必须用 == 判断,而非 errors.Is(除非已包装)
纯哨兵场景下,err == pkg.ErrNotFound 是最直接、最快、语义最明确的写法。errors.Is 是为嵌套错误链设计的,对未包装的哨兵反而多一层间接调用,还可能掩盖误用(比如把局部 errors.New 当哨兵传出去)。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 函数返回纯哨兵:
return user.ErrNotFound→ 调用方写if err == user.ErrNotFound - 函数返回包装哨兵:
return fmt.Errorf("db query failed: %w", user.ErrNotFound)→ 调用方必须用if errors.Is(err, user.ErrNotFound) - 混用会导致逻辑失效:用
errors.Is判断未包装哨兵,虽能工作但不必要;用==判断已包装哨兵,则永远为false -
io.EOF就是典型纯哨兵,标准库文档也明确推荐err == io.EOF
错误信息要简洁、无标点、不含上下文细节
哨兵错误代表一类确定状态,不是日志消息。带句号、空格、时间戳、ID 或路径等动态内容,会破坏其作为“固定值”的契约,也容易让使用者误以为可字符串匹配。
- ✅
"not found"、"invalid argument"、"permission denied" - ❌
"user not found."(句号) - ❌
"user 123 not found"(含 ID,应改用自定义 error 类型) - ❌
"failed to open /tmp/file: permission denied"(含路径和动词,属于包装后错误) - 信息需与变量名语义一致:
ErrNotFound对应"not found",而不是"record missing"—— 否则别人读代码时会困惑
导出名必须首字母大写,且命名遵循 ErrXXX 前缀惯例
小写变量无法被其他包导入,等于没定义哨兵;不用 Err 前缀会打破 Go 生态一致性,IDE 和 linter(如 errcheck)可能无法识别其用途。
- ✅
var ErrInvalidAge = errors.New("age must be between 0 and 150") - ❌
var errNotFound = errors.New("not found")(小写,不可导出) - ❌
var NotFoundError = errors.New("not found")(前缀不标准,linter 可能告警) - 不要加
type后缀,如ErrNotFoundType—— 它不是类型,是值 - 若包有子模块(如
user/internal/repo),哨兵必须定义在主包(user)中,否则使用者得 import 内部路径,破坏封装
真正容易被忽略的不是语法,而是语义边界:一个哨兵只表达一种不可再分的状态。一旦你发现自己想给它加字段、区分原因、或者在测试里反复 errors.Is 套娃判断,就该停一下——那已经不是哨兵该干的活了。

















