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)
}
}
}
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 唯一性校验,是工程落地的关键步骤。