3. 错误码设计原则:唯一性、可读性、可扩展性、层级分类

错误码是系统间通信的“通用语言”,其设计质量直接影响排障效率与系统演进能力。本章围绕四个核心原则展开:唯一性、可读性、可扩展性与层级分类。遵循这些原则,错误码才能从“临时字符串”进化为“可维护的工程资产”。

设计理念:好的错误码不是写给机器看的,而是写给未来 6 个月后的自己和其他开发者看的。断言逻辑的可靠性,首先建立在错误码的清晰定义之上。

3.1 唯一性:每个错误码只代表一种语义

唯一性是错误码设计的基石。一个错误码若对应多种含义,断言逻辑将无法准确判断故障根因。唯一性要求:

  • 全局不重复:整个系统中,同一个数字/字符串编码只对应一种错误场景。
  • 语义不重叠:即使两个模块的错误描述相似(如“用户不存在”与“用户已删除”),也必须分配不同编码。
  • 废弃不重用:已废弃的错误码应永久保留在映射表中,避免新错误码复用旧值导致历史数据污染。
违反唯一性的场景 后果 正确做法
1001 同时表示“参数缺失”和“权限不足” 断言无法区分错误类型,排障需额外日志 1001 → 参数缺失;1002 → 权限不足
2001 在 v1.0 表示“订单超时”,v2.0 表示“库存不足” 旧版本断言逻辑失效,监控告警误报 废弃 2001,新增 2002 表示库存不足
Go 实践建议:使用 const 块集中定义错误码,配合 iota 或显式赋值,避免手写重复值。推荐在包级变量中维护一个 ErrCodeRegistry 映射表,用于启动时校验唯一性。

3.2 可读性:见码知意,降低认知负担

可读性要求错误码在脱离文档的情况下,仍能被开发者快速理解。这包括编码本身的直观性和附带信息的完整性。

  • 分段编码:使用 模块前缀 + 数字序列 或 层级 + 场景 + 序号 结构,如 USR_NOT_FOUND 优于 E0001。
  • 避免纯数字堆砌:40001 不如 ERR_AUTH_TOKEN_EXPIRED 直观;若必须用数字,建议按模块分段(如 4 位:系统 1 位 + 模块 2 位 + 序号 1 位)。
  • 附带人类可读消息:错误码对象应包含 Code 和 Message 两个字段,Message 用于日志和错误展示。
// 可读性良好的错误码定义示例
const (
    // 用户模块错误码 (USR)
    ErrUserNotFound      = "USR_001" // 用户不存在
    ErrUserAlreadyExists = "USR_002" // 用户已存在
    ErrUserPasswordWrong = "USR_003" // 密码错误

    // 订单模块错误码 (ORD)
    ErrOrderNotFound     = "ORD_001" // 订单不存在
    ErrOrderExpired      = "ORD_002" // 订单已过期
)

// 错误码结构体
type ErrCode struct {
    Code    string
    Message string
}

3.3 可扩展性:为未来预留空间

系统会持续迭代,错误码体系必须能容纳新模块、新场景,而不引发大规模重构。可扩展性设计要点:

  • 预留编码区间:每个模块分配一段连续区间(如 1000-1999),中间预留 20% 的空号用于未来扩展。
  • 模块前缀独立:使用字符串前缀(如 AUTH_、PAY_)天然支持无限扩展,无需担心数字区间耗尽。
  • 版本兼容:错误码定义文件应独立于业务代码,新增错误码不修改已有编码,避免影响下游断言逻辑。
常见陷阱:不要将错误码定义在业务函数内部或临时文件中。错误码应集中管理,并作为公共 API 的一部分发布。否则,微服务间断言将因编码不一致而失效。

3.4 层级分类:系统级 vs 业务级

将错误码按层级分类,有助于断言逻辑快速判断错误来源,并采取不同处理策略。通常分为两层:

  • 系统级错误码:基础设施、框架、中间件层面的错误,如网络超时、数据库连接失败、配置缺失。这类错误通常需要运维介入,断言时往往触发告警或降级。
  • 业务级错误码:业务逻辑校验、状态冲突、资源不存在等。这类错误由业务代码主动返回,断言时通常返回给客户端或触发重试。
层级 典型场景 编码范围示例 断言处理策略
系统级 DB 连接超时、RPC 熔断、配置解析失败 SYS_001 ~ SYS_999 记录告警、触发降级、通知运维
业务级 参数校验失败、订单状态冲突、余额不足 BIZ_001 ~ BIZ_999 返回错误响应、提示用户、业务重试

层级分类在断言中的实际价值:

// 根据错误码层级决定处理方式
func handleError(err error) {
    var ec *ErrCode
    if errors.As(err, &ec) {
        if strings.HasPrefix(ec.Code, "SYS_") {
            // 系统级:触发告警,尝试降级
            alertMonitor(ec)
            fallback()
        } else if strings.HasPrefix(ec.Code, "BIZ_") {
            // 业务级:返回客户端,提示用户
            respondWithError(ec)
        }
    }
}
错误码层级分类与断言处理流程 系统级错误码 SYS_001 ~ SYS_999 业务级错误码 BIZ_001 ~ BIZ_999 断言入口 系统级处理 触发告警 · 服务降级 通知运维 · 熔断 业务级处理 返回错误响应 · 提示用户 业务重试 · 状态回滚 断言通过层级前缀快速路由到不同处理策略

3.5 原则落地:错误码定义清单模板

综合以上原则,推荐使用以下模板组织错误码定义文件。该模板已在多个生产项目中验证,兼顾可读性与可扩展性。

// errcode/errcode.go
package errcode

import "fmt"

// 层级常量
const (
    LevelSystem = "SYS"
    LevelBiz    = "BIZ"
)

// ErrCode 统一错误码结构
type ErrCode struct {
    Code    string // 唯一编码,如 "SYS_001"
    Message string // 人类可读描述
}

func (e *ErrCode) Error() string {
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

// 系统级错误码
var (
    ErrDBConnTimeout = &ErrCode{Code: "SYS_001", Message: "数据库连接超时"}
    ErrRPCFailed     = &ErrCode{Code: "SYS_002", Message: "RPC 调用失败"}
    ErrConfigMissing = &ErrCode{Code: "SYS_003", Message: "关键配置缺失"}
)

// 业务级错误码 - 用户模块
var (
    ErrUserNotFound      = &ErrCode{Code: "BIZ_001", Message: "用户不存在"}
    ErrUserAlreadyExists = &ErrCode{Code: "BIZ_002", Message: "用户已存在"}
)

// 业务级错误码 - 订单模块
var (
    ErrOrderNotFound = &ErrCode{Code: "BIZ_010", Message: "订单不存在"}
    ErrOrderExpired  = &ErrCode{Code: "BIZ_011", Message: "订单已过期"}
)
扩展建议:当模块数量超过 10 个时,建议将错误码按模块拆分为独立文件(如 errcode_user.go、errcode_order.go),但仍放在同一个包下,便于全局唯一性校验。

3.6 唯一性校验:自动化保障

人工维护错误码唯一性容易遗漏,建议在单元测试或 CI 阶段加入自动化校验。以下是一个简单的 Go 测试示例:

// errcode/errcode_test.go
package errcode

import (
    "testing"
)

func TestErrCodeUniqueness(t *testing.T) {
    // 收集所有错误码
    codes := map[string]bool{}
    allCodes := []*ErrCode{
        ErrDBConnTimeout, ErrRPCFailed, ErrConfigMissing,
        ErrUserNotFound, ErrUserAlreadyExists,
        ErrOrderNotFound, ErrOrderExpired,
    }

    for _, ec := range allCodes {
        if codes[ec.Code] {
            t.Errorf("重复错误码: %s", ec.Code)
        }
        codes[ec.Code] = true
    }

    // 检查编码格式
    for _, ec := range allCodes {
        if len(ec.Code) != 7 { // 格式: XXX_XXX
            t.Errorf("错误码格式异常: %s", ec.Code)
        }
    }
}

将此类测试加入 CI 流水线,确保每次新增错误码都不会破坏唯一性约束。

错误码设计四大原则关系 错误码设计 唯一性 可读性 可扩展性 层级分类 四大原则共同保障错误码体系的可维护性与断言可靠性 缺失任一原则,错误码断言都将面临风险

3.7 本章小结

  • 唯一性是断言正确性的前提,必须通过自动化校验保障。
  • 可读性降低排障成本,推荐使用分段编码 + 人类可读消息。
  • 可扩展性要求预留编码空间、模块前缀独立、版本兼容。
  • 层级分类(系统级/业务级)让断言逻辑能快速路由到不同处理策略。
  • 将错误码定义集中管理,并纳入 CI 唯一性校验,是工程落地的关键步骤。