哨兵错误是Go中通过包级导出error变量实现精确判断的约定,必须用var定义且不可动态构造;它适用于状态固定、含义单一的失败信号,而需携带上下文时应改用自定义error类型。

什么是哨兵错误,为什么不能用 fmt.Errorf 动态构造
哨兵错误不是语法特性,而是 Go 中一种靠地址唯一性实现精确判断的约定:它必须是包级导出的 error 变量,比如标准库的 io.EOF 或你自定义的 ErrNotFound。核心在于——errors.New("not found") 每次调用都分配新内存,两次调用返回的值地址不同,== 必为 false;而包级变量只初始化一次,所有引用指向同一地址。
常见错误现象:
• 调用方写 if err == ErrNotFound 却始终不成立
• 错误信息明明一样,但判断失败
• 不得已退化成 strings.Contains(err.Error(), "not found"),一改文案就崩
- 必须用
var ErrNotFound = errors.New("not found"),不能写在函数里、不能小写、不能用const - 绝不能在多个文件中各自定义同名哨兵,否则它们是不同变量,
==无效 - 错误消息保持简洁、无标点(如
"user not found"),方便后续组合或日志拼接
err == ErrXXX 和 errors.Is(err, ErrXXX) 到底该用哪个
答案取决于错误是否可能被包装。纯哨兵场景(函数直接返回 ErrNotFound)用 == 更快、更直接;但只要上游用了 fmt.Errorf("failed: %w", ErrNotFound),== 就失效,此时必须用 errors.Is。
容易踩的坑:
• 把 errors.Is 当“更高级替代品”,在未包装时也硬套——多一层递归开销,且掩盖了哨兵本意(显式、轻量、地址相等)
• 在包装链中误用 %s 或 %v 替代 %w,导致 errors.Is 完全找不到原始哨兵
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 未包装、高频路径(如循环读取)→ 优先
err == io.EOF - 明确可能被
%w包装(如中间件、封装层)→ 必须errors.Is(err, pkg.ErrNotFound) - 永远不要对哨兵错误调用
errors.As,它没有字段可提取
如何组织和导出自定义哨兵错误
哨兵错误的生命力依赖于“唯一定义 + 统一引用”。它不是工具函数,不能“调用”,也不能藏在子包或内部结构体里。定义位置决定能否被下游正确比较。
典型错误:
• 把 ErrNotFound 放在 user/internal/repo/ 下,外部包无法导入
• 在 init() 函数里重复赋值,破坏单例性
• 命名模糊,如同时存在 ErrNotExists 和 ErrNotFound,语义重叠难区分
- 统一收口在包主目录的
errors.go文件中,导出(首字母大写) - 按领域分包:数据库错误用
db.ErrNoRows,HTTP 层用http.ErrAbortHandler,避免混杂 - 命名体现责任边界:
json.ErrSyntax是解析失败,os.ErrNotExist是文件系统不存在,二者不可互换 - 不要给哨兵加动态内容(如
fmt.Errorf("user %d not found", id)),那已不属于哨兵范畴
哨兵错误的适用边界在哪
哨兵错误只适合表达「状态固定、含义单一、无需上下文」的失败信号。一旦你需要告诉调用方“哪个 ID 找不到”或“第几行出错”,它就撑不住了——它不携带字段,也不支持嵌套提取。
真实项目里最容易忽略的复杂点:
• 同一个“未找到”在不同层有不同含义:缓存未命中、DB 查无结果、API 返回 404 —— 硬塞同一个 ErrNotFound 会让上层无法区分处理逻辑
• 过度包装:每层都 fmt.Errorf("service: %w", repo.ErrNotFound),最终调用方要写 errors.Is(err, repo.ErrNotFound),包路径越来越深,维护成本飙升
• 错误分类缺失:大量使用 ErrUnknown 或 ErrInternal,本质是错误建模没做好
- 纯状态信号(如流结束、权限拒绝)→ 哨兵合适
- 需携带结构化数据(如
UserID int、StatusCode int)→ 自定义 error 类型 +Unwrap() - 需多层分类响应(如网络超时 vs 连接拒绝)→ 配合
net.IsTimeout(err)等专用判定函数,而非硬造哨兵

















