错误分类与错误码体系:模块级错误码枚举、层级化错误码映射、兼容现有错误

模块间传递错误时,仅靠一个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 中只比较变体类型。

为了更直观理解映射流程,下图展示了错误从底层模块向上冒泡并转换为全局错误码的链路:

层级化错误码映射流程 错误码层级映射链路 用户模块 UserError::NotFound(42) 支付模块 PaymentError::InsufficientBalance 外部依赖模块 DbTimeout 模块 → 全局错误码映射表 忽略动态数据、降级到系统错误 全局错误码 0x01_01_0002 (UserAlreadyExists) 每个模块维护独立的 Map,映射时忽略枚举携带的动态数据,避免 key 爆炸

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 性能与稳定性优化

错误码映射在高频调用路径上可能成为瓶颈,以下是已验证的优化措施:

  1. 使用预编译映射表:将 HashMap 替换为 match 表达式,编译器可生成跳转表。
  2. 错误码缓存:对于频繁出现的同一错误(如NotFound(common_id)),在模块内部缓存其全局错误码。
  3. 零成本降级:映射失败时直接返回预定义的UNKNOWN常量,避免分配新字符串。
  4. 线程安全映射:使用 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
}

3.7 本章总结