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
)
错误码分组架构(模块 + 子域) 用户模块 (11xx) 1100 用户不存在 订单模块 (12xx) 1200 订单未找到 支付模块 (13xx) 1300 支付超时 1101 用户已禁用 1102 密码过期 1201 订单已取消 1202 退款失败 1301 余额不足 1302 渠道拒绝 分组规则 前两位 = 模块编号(11~99) | 后两位 = 子域错误类型(00~99)

3.3 错误码命名规范

错误码的命名直接影响代码可读性与API文档生成质量。遵循以下三段式命名规范:

  1. 前缀:Err + 模块缩写(如 ErrUser、ErrOrder)
  2. 主体:描述错误原因的动词或名词(如 NotFound、Invalid、Expired)
  3. 后缀(可选):细化上下文(如 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 表示访问控制拒绝
常见陷阱:不要将多个断言条件映射到同一个错误码。例如“用户不存在”和“用户已删除”应使用不同错误码,否则无法区分是输入错误还是数据状态异常。
错误码 ↔ 断言边界映射 输入断言 状态断言 资源断言 权限断言 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分钟。每个错误码都是一个可搜索的锚点。