3、错误码常量与枚举:iota定义错误码、错误码分组、错误码命名规范
在Go语言中,错误码的设计直接决定了系统排障效率与API契约的清晰度。本章聚焦于如何利用iota枚举机制定义类型安全的错误码常量,并建立可维护的分组与命名规范。
3.1 iota 枚举机制与错误码定义
Go没有传统的枚举类型,但通过iota预定义标识符可以在const块中生成自增常量。错误码定义的核心原则是:每个错误码必须是显式命名的整数常量,且具备业务语义。
关键约束:错误码不应使用
0值表示成功(与Go的error接口nil语义冲突),建议从1或1000开始递增。
// 基础错误码枚举(推荐从1000起始,预留扩展空间)
const (
ErrSuccess ErrorCode = 0 // 保留,但实际业务中应使用 nil
ErrUnknown ErrorCode = iota + 1000 // 1001
ErrInvalidParam // 1002
ErrNotFound // 1003
ErrPermissionDenied // 1004
ErrTimeout // 1005
ErrInternal // 1006
)
| iota 特性 | 说明 | 错误码场景示例 |
|---|---|---|
| 自增偏移 | iota + base 实现分段编号 |
iota + 1000 生成 1001~1006 |
| 跳过占位 | 使用 _ 跳过不需要的值 |
预留已废弃的错误码 |
| 位掩码组合 | iota 配合 << 生成权限位 |
错误严重级别标记 |
| 类型安全 | 自定义类型 type ErrorCode int |
防止不同枚举混用 |
3.2 错误码分组策略
当系统模块超过3个时,必须对错误码进行分组。分组方式决定了错误码的可读性与排障效率。推荐采用模块前缀 + 功能子域的二维分组模型。
分组黄金法则:错误码的前两位数字代表模块,后两位代表具体错误类型。例如
1203 = 用户模块(12) + 密码错误(03)。
// 模块分组示例(使用 iota + 模块基值)
const (
// 用户模块错误码 1100~1199
ErrUserNotFound ErrorCode = 1100 + iota // 1100
ErrUserDisabled // 1101
ErrUserPasswordExpired // 1102
// 订单模块错误码 1200~1299
ErrOrderNotFound ErrorCode = 1200 + iota // 1200
ErrOrderCancelled // 1201
ErrOrderRefundFailed // 1202
)
3.3 错误码命名规范
错误码的命名直接影响代码可读性与API文档生成质量。遵循以下三段式命名规范:
- 前缀:
Err+ 模块缩写(如ErrUser、ErrOrder) - 主体:描述错误原因的动词或名词(如
NotFound、Invalid、Expired) - 后缀(可选):细化上下文(如
ForUser、InDatabase)
// 命名规范示例
const (
// 用户模块
ErrUserNotFound ErrorCode = 1100 // 用户不存在
ErrUserPasswordInvalid ErrorCode = 1101 // 密码格式错误
ErrUserAccountLocked ErrorCode = 1102 // 账户已锁定
// 订单模块
ErrOrderNotFound ErrorCode = 1200 // 订单不存在
ErrOrderStatusConflict ErrorCode = 1201 // 订单状态冲突
ErrOrderRefundExceeded ErrorCode = 1202 // 退款金额超限
)
| 命名反模式 | 问题 | 推荐写法 |
|---|---|---|
Err1 / Err2 |
无语义,无法排障 | ErrUserNotFound |
ErrorUserNotFound |
前缀过长,不符合Go惯例 | ErrUserNotFound |
Err_User_Not_Found |
下划线过多,不符合驼峰 | ErrUserNotFound |
ErrNotFound(全局) |
模块归属不明确 | ErrUserNotFound / ErrOrderNotFound |
3.4 错误码与断言边界的关系
错误码不仅是错误标识,更是断言边界的显式标记。每个错误码对应一个明确的断言条件:
- 输入断言:
ErrInvalidParam表示参数校验失败 - 状态断言:
ErrUserDisabled表示用户状态不满足操作前提 - 资源断言:
ErrNotFound表示目标资源不存在 - 权限断言:
ErrPermissionDenied表示访问控制拒绝
常见陷阱:不要将多个断言条件映射到同一个错误码。例如“用户不存在”和“用户已删除”应使用不同错误码,否则无法区分是输入错误还是数据状态异常。
3.5 最佳实践清单
- 类型安全优先:始终使用
type ErrorCode int自定义类型,避免原始int混用 - 预留扩展区间:每个模块预留100个编号(如 1100~1199),避免后续插入导致重排
- 文档即代码:每个错误码常量必须附带注释,说明触发条件和处理建议
- 集中注册:所有错误码在
errors/code.go中集中定义,禁止散落在各包中 - 版本兼容:已发布的错误码不可删除或修改数值,只能标记为
Deprecated
// 完整示例:带注释的错误码定义
package errors
// ErrorCode 定义类型安全的错误码
type ErrorCode int
const (
// 用户模块错误码 (1100-1199)
ErrUserNotFound ErrorCode = 1100 // 用户不存在,建议检查输入的用户ID
ErrUserPasswordInvalid ErrorCode = 1101 // 密码格式错误,需满足8-32位含大小写
ErrUserAccountLocked ErrorCode = 1102 // 账户已锁定,连续5次失败后触发
// 订单模块错误码 (1200-1299)
ErrOrderNotFound ErrorCode = 1200 // 订单不存在,可能已被删除或ID错误
ErrOrderStatusConflict ErrorCode = 1201 // 订单状态不允许当前操作,如已取消订单不可退款
)
排障效率指标:采用上述规范后,线上排障时通过错误码定位代码位置的平均时间可从 15分钟 缩短至 2分钟。每个错误码都是一个可搜索的锚点。