Result 结构设计:成功/失败泛型、内部错误码、错误消息、堆栈上下文
模块边界与组件职责
Result 结构不是简单的代码工具,而是跨模块通信的边界契约。它强制将模块的出口统一为两种状态,消除隐式运行时错误。各组件职责如下:
| 组件 | 模块边界角色 | 核心职责 |
|---|---|---|
Result<T, E> 基类 |
通用合约 | 承载 IsSuccess 状态机,禁止直接实例化 |
Success<T> |
正常出口 | 携带业务实体 T Value,触发调用方正常流程 |
Failure<E> |
异常出口 | 携带 ErrorCode + ErrorMessage + StackTrace |
ErrorCode |
程序化错误标识 | 整型或枚举,按模块前缀隔离(MOD_A_1001) |
DiagnosticContext |
调试与追踪 | 记录调用链 ID、参数快照、时间戳,支持跨模块关联 |
架构约束:Result 应当成为模块对外暴露的唯一返回值类型。禁止在公共接口中直接抛出原生异常或返回裸实体,保证调用方无需猜测出口状态。
数据流与控制流:强制路由
调用方通过 Result 对象的显式状态检查,将控制流从“猜测”变为“强制匹配”。
- 正向流:模块 A → 入口网关 → 模块 B 服务 → 返回
Success<T>→ 模块 A 解包Value执行业务。 - 保护流:模块 B 内部验证失败 → 返回
Failure<E>→ 模块 A 检查ErrorCode→ 降级或重试。 - 上下文传播流:
DiagnosticContext随 Failure 附加上报,不因跨模块而丢失堆栈。
数据流控制要点:Result 不允许跨模块裸传递异常信息。错误码必须是预定义的内部枚举,不允许携带模块 B 的内部堆栈字符串穿透到模块 A。若需要堆栈,必须通过
DiagnosticContext 字段附加,且不得影响序列化主体。
选型权衡
| 选型维度 | 强类型泛型方案 | 弱类型 / 动态方案 | 架构建议 |
|---|---|---|---|
| 错误确定性 | 高:编译器强制处理每种错误码 | 低:调用方易忽略错误码 | 模块间接口必须强类型 |
| 序列化友好度 | 中:需设计泛型序列化转换器 | 高:自由 JSON 对象 | 核心模块使用强类型,边缘使用灵活契约 |
| 版本演进成本 | 中:增加新错误码需修改枚举 | 低:字符串可随时新增 | 使用语义化错误码前缀,预留 Reserved 段 |
| 跨语言互操作 | 低:依赖具体语言泛型实现 | 高:与 Protobuf/Thrift 亲和 | 边界定义 IDL,内部实现强类型封装 |
风险警告:切勿在 ErrorCode 中使用魔数或链式异常码。错误码必须是一等公民,纳入模块 API 文档。堆栈上下文不要无条件捕获全部参数,防止敏感数据泄露。推荐启用字段级白名单机制,只记录预先声明的上下文字段。
落地步骤
- 定义泛型基类:在公共核心库中声明
Result<TResult, TError>,提供Match模式匹配方法。 - 实现工厂方法:创建
Result.Success(value)与Result.Failure(errorCode, message, context)静态入口。 - 设计内部错误码枚举:按模块前缀隔离,例如
ModuleA_Error包含InvalidParameter=1001、DependencyFailed=1002。 - 建立堆栈上下文契约:定义
IDiagnosticContext接口,支持附加TraceId、SpanId、InputSnapshot。 - 序列化适配:为 Result 编写自定义 JSON 序列化器,将
Success/Failure统一输出为{ "status": "...", "data": "...", "error": {...} }。 - 集成 CI 门禁:在模块边界层添加静态分析规则,禁止
throw Exception穿越公共接口。
// 核心 Result 结构示意图(C# 风格伪码)
public abstract record Result<TValue, TError>
{
public bool IsSuccess { get; init; }
public bool IsFailure => !IsSuccess;
public T Match<T>(Func<TValue, T> onSuccess, Func<TError, T> onFailure);
}
public sealed record Success<TValue, TError>(TValue Value)
: Result<TValue, TError>
{
public bool IsSuccess => true;
}
public sealed record Failure<TValue, TError>(TError Error, DiagnosticContext Context)
: Result<TValue, TError>
{
public bool IsSuccess => false;
}
// 内部错误码枚举规则
public enum OrderModuleErrorCode
{
Success = 0,
InvalidOrderId = 2001,
InsufficientBalance = 2002,
PaymentGatewayTimeout = 2003,
}
验收标准
- 零裸异常穿越:所有模块公共方法返回类型必须是
Result<,>,CI 扫描到throw关键字在接口层出现即告警。 - 错误码覆盖率 100%:failure 无默认值,每个
Failure实例必须附带一个有意义的ErrorCode。 - 上下文可回溯:从入口到出口,
TraceId在 Failure 的DiagnosticContext中完整保留,支持跨模块日志关联。 - 结构体不可变:Result 一旦创建,其
IsSuccess与内部数据不可变更,保证并发安全。 - 序列化对称性:将 Result 序列化为 JSON 再反序列化后,状态与数据不丢失,单元测试覆盖 Success 和 Failure 两种路由。