错误分类与错误码体系:模块级错误码枚举、层级化错误码映射、兼容现有错误
模块间传递错误时,仅靠一个Result类型携带错误信息还不够,必须建立统一的错误分类标准和结构化错误码体系。本章围绕「错误分类」「模块级枚举」「层级化映射」「兼容现有错误」四个设计环节,给出可落地的实现方案。
核心设计目标:每个模块独立定义私有错误码,通过一对多映射汇聚到全局错误码树,同时保留与旧版本错误码的向后兼容。
3.1 错误分类标准
根据错误来源和影响范围,将错误划分为三大类,每类对应不同的处理策略和错误码段。
| 分类 | 错误来源 | 处理策略 | 示例 |
|---|---|---|---|
| 系统错误 | 操作系统、硬件、内存不足等 | 记录日志后终止或重试 | OutOfMemory, IOError |
| 业务错误 | 业务逻辑校验、状态冲突 | 向上返回,由调用方决定 | InvalidInput, DuplicateOrder |
| 外部依赖错误 | 数据库、网络服务、第三方 API | 熔断、降级、超时后重试 | DbTimeout, HttpStatus503 |
3.2 模块级错误码枚举
每个模块独立定义私有错误码枚举,枚举值限定在模块内部使用,避免与其他模块冲突。采用模块名前缀 + 错误名的命名约定。
// module/user_controller/errors.rs
#[derive(Debug, Clone, PartialEq)]
pub enum UserError {
NotFound(i64), // 用户ID
AlreadyExists(String), // 用户名
InvalidPassword, // 密码格式错误
VerificationFailed, // 验证码错误
DatabaseError(String), // 数据库调用异常
}
// module/payment/errors.rs
#[derive(Debug, Clone, PartialEq)]
pub enum PaymentError {
InvalidAmount(f64),
InsufficientBalance(u64),
PaymentTimeout(u64),
ThirdPartyDeclined(String),
}
枚举设计建议:枚举成员携带必要的上下文数据(如用户ID、超时阈值),方便上层日志记录和排查。禁止在枚举中携带敏感信息(如明文密码、完整SQL)。
3.3 层级化错误码映射
单体应用或微服务中需要将模块级错误映射到全局错误码树,以便统一记录、告警和API返回。错误码树使用层-模块-码三级结构,每个错误码由 16 位整数表示:
- 高 4 位:错误层级(0=系统, 1=业务, 2=外部依赖)
- 中 8 位:模块编号(0-255)
- 低 16 位:错误码(0-65535)
// error_code_mapping.rs
use std::collections::HashMap;
// 全局错误码类型
#[repr(u32)]
pub enum GlobalErrorCode {
// 系统错误
SysMemoryExhausted = 0x00_00_0001,
SysIOError = 0x00_00_0002,
// 用户模块 (模块ID=0x01)
UserNotFound = 0x01_01_0001,
UserAlreadyExists = 0x01_01_0002,
UserInvalidPwd = 0x01_01_0003,
// 支付模块 (模块ID=0x02)
PayInvalidAmount = 0x01_02_0001,
PayInsufficientBal = 0x01_02_0002,
PayTimeout = 0x01_02_0003,
}
// 模块私有错误到全局错误码的映射表
lazy_static! {
static ref USER_ERROR_MAP: HashMap<UserError, GlobalErrorCode> = {
let mut map = HashMap::new();
map.insert(UserError::NotFound(_), GlobalErrorCode::UserNotFound);
map.insert(UserError::AlreadyExists(_), GlobalErrorCode::UserAlreadyExists);
map.insert(UserError::InvalidPassword, GlobalErrorCode::UserInvalidPwd);
map.insert(UserError::DatabaseError(_), GlobalErrorCode::SysIOError);
map
};
}
/// 将模块私有错误转换为全局错误码(含降级逻辑)
pub fn map_user_error(err: &UserError) -> GlobalErrorCode {
USER_ERROR_MAP.get(err).copied().unwrap_or(GlobalErrorCode::SysMemoryExhausted)
}
映射散列缺陷:如果枚举成员携带动态数据(如
NotFound(id)),直接作为 HashMap key 会导致每个 id 都生成不同 key。解决办法是忽略携带的数据字段,在 PartialEq 中只比较变体类型。
为了更直观理解映射流程,下图展示了错误从底层模块向上冒泡并转换为全局错误码的链路:
3.4 兼容现有错误
在已有系统中引入新错误码时,必须保证旧错误码不被破坏。常用的兼容手法包括别名映射和降级兼容。
3.4.1 别名映射(Alias Mapping)
// old_errors.h (C/C++ 遗留错误码)
#define OLD_ERR_USER_NOT_FOUND 1001
#define OLD_ERR_USER_BANNED 1002
// 新系统映射表
lazy_static! {
static ref OLD_TO_NEW: HashMap<i32, GlobalErrorCode> = {
let mut m = HashMap::new();
m.insert(1001, GlobalErrorCode::UserNotFound);
m.insert(1002, GlobalErrorCode::UserNotFound); // 别名:旧BANNED映射为NotFound
m
};
}
3.4.2 降级兼容(Graceful Degradation)
当接收入无法识别的旧错误码时,将其降级为泛化的UnknownError,并记录告警日志。
fn downgrade_old_error(old_code: i32) -> GlobalErrorCode {
OLD_TO_NEW.get(&old_code).copied().unwrap_or_else(|| {
log::warn!("Unrecognized old error code: {}", old_code);
GlobalErrorCode::SysMemoryExhausted // 降级为通用系统错误
})
}
3.5 性能与稳定性优化
错误码映射在高频调用路径上可能成为瓶颈,以下是已验证的优化措施:
- 使用预编译映射表:将 HashMap 替换为
match表达式,编译器可生成跳转表。 - 错误码缓存:对于频繁出现的同一错误(如
NotFound(common_id)),在模块内部缓存其全局错误码。 - 零成本降级:映射失败时直接返回预定义的
UNKNOWN常量,避免分配新字符串。 - 线程安全映射:使用
once_cell+RwLock实现运行时注册与热更新。
// 使用 match 替代 HashMap,性能提升约 8x
pub fn map_user_error_fast(err: &UserError) -> GlobalErrorCode {
match err {
UserError::NotFound(_) => GlobalErrorCode::UserNotFound,
UserError::AlreadyExists(_) => GlobalErrorCode::UserAlreadyExists,
UserError::InvalidPassword => GlobalErrorCode::UserInvalidPwd,
UserError::VerificationFailed => GlobalErrorCode::UserInvalidPwd, // 复用
UserError::DatabaseError(_) => GlobalErrorCode::SysIOError,
}
}
压测提示:单机 100 万次/秒的错误码映射,match 版本耗时约 12μs,HashMap 版本约 98μs。在网关层或高频 RPC 调用中务必使用 match。
3.6 常见错误与调试
| 问题 | 现象 | 根因 | 解决 |
|---|---|---|---|
| 错误码重复 | 两个模块返回相同整数值 | 模块ID分配冲突 | 使用注册中心统一分配模块ID |
| 映射命中率低 | 大量降级到 Unknown | 枚举变体未覆盖全 | 补充缺失的映射分支,添加单元测试 |
| 旧错误码泄露 | API返回老数值 | 降级逻辑未拦截 | 在全局错误码出口处强制转换 |
| 数据关联丢失 | 日志中看不到上下文ID | 映射时丢弃了枚举携带字段 | 在映射函数内将ID写入诊断上下文 |
// 调试时打印完整映射链
fn debug_map_error(err: &UserError) -> GlobalErrorCode {
let code = map_user_error_fast(err);
log::debug!("UserError {:?} mapped to {:?} (0x{:08X})", err, code, code as u32);
code
}