All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m36s
主要改动: - 改造 errors.New() 和 Wrap() 函数签名为可变参数,优先使用 errorMessages 映射表 - 添加 allErrorCodes 注册表和 init() 启动时校验,确保错误码与映射表一致 - 添加 TestAllCodesHaveMessages 和 TestNoOrphanMessages 测试防止映射表腐化 - 清理 109 处与映射表一致的冗余硬编码(service 层) - 保留业务特定消息覆盖能力 新增 API 用法: - errors.New(errors.CodeUnauthorized) // 使用映射表默认消息 - errors.New(errors.CodeNotFound, "提现申请不存在") // 覆盖为自定义消息
7.3 KiB
7.3 KiB
Context
背景
项目中错误处理使用 pkg/errors/ 包,核心组件:
| 组件 | 职责 |
|---|---|
codes.go |
定义错误码常量 Code* 和消息映射表 errorMessages |
errors.go |
定义 AppError 结构和 New()/Wrap() 构造函数 |
handler.go |
Fiber 全局 ErrorHandler,处理错误响应 |
当前问题
// errors.go 的设计意图:当 message 为空时自动使用映射表
func New(code int, message string) *AppError {
if message == "" {
message = GetMessage(code, "zh-CN") // 理论上的自动填充
}
return &AppError{Code: code, Message: message}
}
// 实际业务代码:100% 传入硬编码消息,映射表从未被使用
errors.New(errors.CodeNotFound, "提现申请不存在") // 不是空字符串
量化数据(通过 grep 统计):
errors.New(code, "硬编码")调用:291 处errors.New(code, "")调用:0 处- 映射表使用率:0%
约束
- 必须向后兼容,不能破坏现有 API 响应格式
- 业务特定消息(如 "提现申请不存在")优于通用消息("资源未找到")
- 不能引入运行时性能损耗
Goals / Non-Goals
Goals
- 单一数据源原则:错误码与消息映射表作为默认消息来源
- 编译时/启动时校验:缺失映射条目会立即暴露,而非运行时默默失败
- 向后兼容:保留业务特定消息覆盖能力
- 开发体验优化:简化 API,减少样板代码
Non-Goals
- ❌ 多语言支持(保留
lang参数但不实现) - ❌ 修改 API 响应格式
- ❌ 强制所有错误使用映射表消息(保留覆盖能力)
Decisions
Decision 1: 改造 errors.New() 函数签名
选项:
| 选项 | 实现 | 优缺点 |
|---|---|---|
| A. 删除 message 参数 | New(code int) |
✅ 强制单一数据源 ❌ 丢失业务上下文 |
| B. message 改为可选 | New(code int, msg ...string) |
✅ 默认用映射表 ✅ 允许覆盖 ✅ 向后兼容 |
| C. 新增 NewWithMsg | New(code) + NewWithMsg(code, msg) |
✅ 清晰区分 ❌ 需改所有调用点 |
决策:选项 B - 使用可变参数
// 新签名
func New(code int, customMsg ...string) *AppError {
msg := GetMessage(code, "zh-CN") // 默认从映射表取
if len(customMsg) > 0 && customMsg[0] != "" {
msg = customMsg[0] // 允许覆盖
}
return &AppError{Code: code, Message: msg}
}
// 使用方式
errors.New(errors.CodeNotFound) // 使用映射表: "资源未找到"
errors.New(errors.CodeNotFound, "提现申请不存在") // 覆盖: "提现申请不存在"
理由:
- 向后兼容:现有代码无需修改即可编译
- 渐进式迁移:可逐步清理冗余硬编码
- 保留灵活性:业务特定消息仍可使用
Decision 2: 启动时校验机制
选项:
| 选项 | 时机 | 优缺点 |
|---|---|---|
| A. init() panic | 程序启动 | ✅ 立即发现 ❌ 启动失败 |
| B. init() 日志警告 | 程序启动 | ✅ 不阻塞启动 ❌ 可能被忽略 |
| C. 仅测试校验 | CI 运行 | ✅ 不影响生产 ❌ 本地开发可能遗漏 |
决策:选项 A + C 组合
- init() 严格校验:缺失映射立即 panic,阻止服务启动
- CI 测试兜底:测试覆盖所有错误码,防止遗漏
func init() {
for code := range allCodes() { // 遍历所有 Code* 常量
if _, ok := errorMessages[code]; !ok {
panic(fmt.Sprintf("错误码 %d 缺少映射消息", code))
}
}
}
理由:
- 快速失败:问题在部署前暴露,而非运行时
- 强制一致性:不允许"遗漏"状态存在
Decision 3: 错误码常量收集方式
选项:
| 选项 | 实现 | 优缺点 |
|---|---|---|
| A. 手动维护列表 | allCodes = []int{CodeSuccess, CodeNotFound, ...} |
❌ 容易忘记更新 |
| B. 反射扫描 | 运行时反射遍历 const | ❌ Go 不支持反射 const |
| C. 代码生成 | go generate 扫描生成 |
✅ 自动化 ❌ 增加构建复杂度 |
| D. 基于映射表反向校验 | 遍历 errorMessages 的 key | ✅ 简单 ❌ 无法检测缺失 |
| E. 定义错误码注册表 | 显式注册每个错误码 | ✅ 清晰 ✅ 编译时检查 |
决策:选项 E - 定义完整的错误码切片
// 所有错误码必须在此列表中注册
var allErrorCodes = []int{
CodeSuccess,
CodeInvalidParam,
CodeMissingToken,
// ... 所有错误码
}
func init() {
for _, code := range allErrorCodes {
if _, ok := errorMessages[code]; !ok {
panic(fmt.Sprintf("错误码 %d 缺少映射消息", code))
}
}
}
理由:
- 显式优于隐式:所有错误码一目了然
- 新增错误码时必须同时更新两处(常量 + 列表),测试会强制检查映射表
Decision 4: 业务代码清理策略
选项:
| 选项 | 范围 | 优缺点 |
|---|---|---|
| A. 全量清理 | 所有 291 处 | ✅ 彻底 ❌ 风险高,改动大 |
| B. 仅清理冗余 | 消息与映射表一致的 | ✅ 安全 ✅ 保留业务上下文 |
| C. 不清理 | 保持现状 | ✅ 零风险 ❌ 技术债未还 |
决策:选项 B - 仅清理冗余硬编码
清理规则:
// 清理前:消息与映射表完全一致
errors.New(errors.CodeUnauthorized, "未授权访问")
// 清理后:使用映射表默认值
errors.New(errors.CodeUnauthorized)
// 保留:业务特定消息,比映射表更精确
errors.New(errors.CodeNotFound, "提现申请不存在") // 保留,比 "资源未找到" 更清晰
理由:
- 最小改动原则:只改必要的
- 保留业务价值:精确消息优于通用消息
Risks / Trade-offs
风险矩阵
| 风险 | 等级 | 缓解措施 |
|---|---|---|
| 启动时 panic 影响部署 | 中 | 本地开发和 CI 会先发现;错误消息清晰指明缺失的错误码 |
| 消息文案变化影响前端 | 低 | 仅清理与映射表一致的;业务特定消息保留 |
| 大规模代码改动引入 bug | 中 | 分阶段实施:先加校验和测试,再清理代码 |
| 遗漏错误码导致 panic | 低 | allErrorCodes 列表 + 测试双重保障 |
Trade-offs
-
启动时 panic vs 运行时容错
- 选择 panic:快速失败,问题在部署前暴露
- 代价:如果遗漏会阻止服务启动
-
强制映射表 vs 允许覆盖
- 选择允许覆盖:保留业务灵活性
- 代价:无法 100% 保证消息一致性
Migration Plan
Phase 1: 基础设施(本次实施)
- 改造
errors.New()函数签名(向后兼容) - 添加
allErrorCodes注册表 - 添加
init()启动校验 - 添加
TestAllCodesHaveMessages测试 - 补充可能缺失的 errorMessages 条目
Phase 2: 代码清理(本次实施)
- 清理与映射表一致的冗余硬编码
- 保留业务特定消息
Rollback Strategy
如出现问题,回滚方式:
errors.New()签名改动是向后兼容的,无需回滚- 如果 init() panic 导致问题,临时注释校验代码
- Git revert 整个变更
Open Questions
- 是否需要 linter 规则?
- 可考虑添加 golangci-lint 自定义规则,检测
errors.New(code, msg)中 msg 与映射表重复的情况 - 暂不实施,后续根据需要添加
- 可考虑添加 golangci-lint 自定义规则,检测