Files
junhong_cmp_fiber/docs/003-error-handling/架构说明.md
huang fb83c9a706 feat: 实现统一错误处理系统 (003-error-handling)
- 新增统一错误码定义和管理 (pkg/errors/codes.go)
- 新增全局错误处理器和中间件 (pkg/errors/handler.go, internal/middleware/error_handler.go)
- 新增错误上下文管理 (pkg/errors/context.go)
- 增强 Panic 恢复中间件 (internal/middleware/recover.go)
- 新增完整的单元测试和集成测试
- 新增功能文档 (docs/003-error-handling/)
- 新增功能规范 (specs/003-error-handling/)
- 更新 CLAUDE.md 和 README.md
2025-11-15 12:17:44 +08:00

788 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构说明Fiber 错误处理集成
**功能编号**: 003-error-handling
**版本**: 1.0.0
**更新日期**: 2025-11-15
## 目录
1. [架构概览](#架构概览)
2. [核心组件](#核心组件)
3. [错误处理流程](#错误处理流程)
4. [设计决策](#设计决策)
5. [性能优化](#性能优化)
6. [扩展性设计](#扩展性设计)
---
## 架构概览
### 整体架构图
```
┌─────────────────────────────────────────────────────────────┐
│ Fiber Application │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Middleware Chain │
│ ┌────────────┐ ┌───────────┐ ┌────────┐ ┌──────────┐ │
│ │ Recover │→ │ RequestID │→ │ Logger │→ │ ... │ │
│ └────────────┘ └───────────┘ └────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Handlers │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ if err != nil { │ │
│ │ return errors.New(code, msg) ──────┐ │ │
│ │ } │ │ │
│ └─────────────────────────────────────────┼────────────┘ │
└──────────────────────────────────────────┼──────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Global ErrorHandler │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 响应状态检查 │ │
│ │ 2. 错误类型分类 (*AppError, *fiber.Error, error) │ │
│ │ 3. 提取错误上下文 (FromFiberContext) │ │
│ │ 4. 错误消息脱敏 (5xx → 通用消息) │ │
│ │ 5. 记录日志 (按级别: Warn/Error) │ │
│ │ 6. 构造 JSON 响应 │ │
│ │ 7. 设置 X-Request-ID Header │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Client Response │
│ { │
│ "code": 1001, │
│ "data": null, │
│ "msg": "参数验证失败", │
│ "timestamp": "2025-11-15T10:00:00+08:00" │
│ } │
│ X-Request-ID: uuid │
└─────────────────────────────────────────────────────────────┘
```
### 数据流图
```
Request
├─→ Recover Middleware ──[panic]──→ AppError(Code2001)
│ │
├─→ RequestID Middleware ──[生成 UUID]───→ c.Locals("requestid")
│ │
├─→ Handler ──[返回错误]──→ AppError/fiber.Error/error
│ │
└───────────────────────────────────────→ ErrorHandler
├─→ ErrorContext.FromFiberContext()
│ (提取 Request ID, 路径, 参数等)
├─→ GetLogLevel(code)
│ (确定日志级别)
├─→ 脱敏逻辑
│ (5xx → "内部服务器错误")
├─→ Logger.Warn/Error()
│ (记录到日志文件)
└─→ c.Status(httpStatus).JSON(response)
(返回统一格式)
```
---
## 核心组件
### 1. 错误码系统 (`pkg/errors/codes.go`)
**职责**: 定义标准错误码和映射规则
**设计原则**:
- 错误码分段管理(成功=0客户端=1xxx服务端=2xxx
- 每个错误码有固定的 HTTP 状态码和日志级别
- 支持多语言错误消息(当前支持中文)
**核心数据结构**:
```go
const (
CodeSuccess = 0
CodeInvalidParam = 1001 // 客户端错误
CodeDatabaseError = 2002 // 服务端错误
)
// 错误消息映射
var errorMessages = map[int]map[string]string{
CodeSuccess: {"zh": "操作成功"},
CodeInvalidParam: {"zh": "参数验证失败"},
}
// HTTP 状态码映射
func GetHTTPStatus(code int) int
// 日志级别映射
func GetLogLevel(code int) string
```
**扩展性**:
- 新增错误码:在对应范围内添加常量和消息映射
- 新增语言:在 `errorMessages` 中添加语言键
---
### 2. 应用错误类型 (`pkg/errors/errors.go`)
**职责**: 封装业务错误,支持错误链
**设计原则**:
- 实现标准 `error` 接口
- 支持错误包装 (`Unwrap()`)
- 自动关联 HTTP 状态码
**核心数据结构**:
```go
type AppError struct {
Code int // 应用错误码
Message string // 用户可见消息
HTTPStatus int // HTTP 状态码(自动映射)
Err error // 底层错误(可选)
}
func (e *AppError) Error() string // 实现 error 接口
func (e *AppError) Unwrap() error // 支持 errors.Unwrap()
func (e *AppError) WithHTTPStatus(int) *AppError // 覆盖状态码
```
**使用模式**:
```go
// 创建新错误
err := errors.New(errors.CodeNotFound, "用户不存在")
// 包装现有错误
err := errors.Wrap(errors.CodeDatabaseError, "查询失败", dbErr)
// 自定义状态码
err := errors.New(errors.CodeInvalidParam, "验证失败").WithHTTPStatus(422)
```
---
### 3. 错误上下文 (`pkg/errors/context.go`)
**职责**: 提取和管理请求上下文信息
**设计原则**:
- 从 Fiber Context 自动提取
- 转换为结构化日志字段
- 包含调试所需的所有信息
**核心数据结构**:
```go
type ErrorContext struct {
RequestID string
Method string
Path string
Query string
IP string
UserAgent string
UserID string // 如果已认证
}
func FromFiberContext(c *fiber.Ctx) *ErrorContext
func (ec *ErrorContext) ToLogFields() []zap.Field
```
**信息提取逻辑**:
```go
RequestID c.Locals("requestid") // 由 RequestID 中间件设置
Method c.Method()
Path c.Path()
Query c.Request().URI().QueryArgs()
IP c.IP()
UserAgent c.Get("User-Agent")
UserID c.Locals("user_id") // 由认证中间件设置
```
---
### 4. 全局错误处理器 (`pkg/errors/handler.go`)
**职责**: 统一处理所有错误,生成标准响应
**设计原则**:
- 单一入口,统一格式
- 自身保护(防止 ErrorHandler panic
- 敏感信息脱敏
**核心逻辑**:
```go
func SafeErrorHandler() fiber.ErrorHandler {
return func(c *fiber.Ctx, err error) error {
defer func() {
if r := recover(); r != nil {
// ErrorHandler 自身保护
fallbackError(c)
}
}()
return handleError(c, err)
}
}
func handleError(c *fiber.Ctx, err error) error {
// 1. 响应状态检查
if c.Response().StatusCode() != fiber.StatusOK {
return nil // 已发送响应,避免重复处理
}
// 2. 错误类型分类
var (
code int
message string
httpStatus int
)
switch e := err.(type) {
case *AppError:
code = e.Code
message = e.Message
httpStatus = e.HTTPStatus
case *fiber.Error:
code = mapHTTPStatusToCode(e.Code)
message = e.Message
httpStatus = e.Code
default:
code = CodeInternalError
message = "内部服务器错误"
httpStatus = 500
}
// 3. 敏感信息脱敏
if httpStatus >= 500 {
message = GetMessage(code, "zh") // 使用通用消息
}
// 4. 提取错误上下文
errCtx := FromFiberContext(c)
// 5. 记录日志
logLevel := GetLogLevel(code)
if logLevel == "error" {
logger.Error("服务端错误", errCtx.ToLogFields()...)
} else {
logger.Warn("客户端错误", errCtx.ToLogFields()...)
}
// 6. 构造响应
response := fiber.Map{
"code": code,
"data": nil,
"msg": message,
"timestamp": time.Now().Format(time.RFC3339),
}
// 7. 设置 Header
c.Set("X-Request-ID", errCtx.RequestID)
return c.Status(httpStatus).JSON(response)
}
```
---
### 5. Panic 恢复中间件 (`internal/middleware/recover.go`)
**职责**: 捕获 panic防止服务崩溃
**设计原则**:
- 第一层防护,必须最先注册
- 完整堆栈跟踪
- 转换为标准错误
**核心逻辑**:
```go
func Recover(logger *zap.Logger) fiber.Handler {
return func(c *fiber.Ctx) error {
defer func() {
if r := recover(); r != nil {
// 1. 捕获堆栈跟踪
stack := debug.Stack()
// 2. 记录详细日志
logger.Error("panic recovered",
zap.Any("panic", r),
zap.String("stack", string(stack)),
zap.String("request_id", c.Locals("requestid").(string)),
)
// 3. 转换为 AppError
err := &errors.AppError{
Code: errors.CodeInternalError,
Message: "服务发生异常",
HTTPStatus: 500,
}
// 4. 委托给 ErrorHandler 处理
c.Next() // 触发 ErrorHandler
}
}()
return c.Next()
}
}
```
---
## 错误处理流程
### 正常错误流程
```
1. Handler 返回错误
2. Fiber 调用 ErrorHandler
3. ErrorHandler 分类错误
4. 提取错误上下文
5. 确定日志级别
6. 脱敏处理(如果是 5xx
7. 记录日志
8. 构造 JSON 响应
9. 返回给客户端
```
### Panic 处理流程
```
1. Handler 发生 panic
2. Recover 中间件捕获
3. 记录完整堆栈到日志
4. 转换为 AppError(Code2001)
5. 委托给 ErrorHandler 处理
6. 返回 500 错误响应
```
### 并发处理保障
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│Request 1│ │Request 2│ │Request 3│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
├─→ Goroutine 1 ├─→ Goroutine 2 ├─→ Goroutine 3
│ │ │
│ (独立 Fiber Ctx, 独立 defer/recover)
│ │ │
▼ ▼ ▼
正常响应 Panic 捕获 错误响应
```
每个请求在独立的 Goroutine 中处理,拥有独立的:
- Fiber Context
- defer/recover 堆栈
- 错误处理流程
**保证**: 单个请求的 panic 不会影响其他请求。
---
## 设计决策
### 1. 为什么使用错误码而不是 HTTP 状态码?
**问题**: HTTP 状态码不足以表达业务语义
**示例**:
- 400 Bad Request: 参数格式错误?缺失字段?验证失败?
- 401 Unauthorized: 缺少 TokenToken 无效Token 过期?
**解决方案**:
- 引入应用错误码1001, 1002, ...
- 每个错误码有明确的业务含义
- HTTP 状态码仅用于 HTTP 层分类4xx/5xx
**好处**:
- 客户端可精确识别错误类型
- 支持多语言错误消息
- 便于统计和监控
---
### 2. 为什么 ErrorHandler 不依赖 `pkg/response`
**问题**: 循环依赖
```
pkg/response ──imports──> pkg/errors
↑ │
└───────imports───────────┘ (循环!)
```
**解决方案**: ErrorHandler 直接使用 `fiber.Map`
```go
// 不使用 response.Error()
return c.Status(500).JSON(fiber.Map{
"code": code,
"data": nil,
"msg": message,
"timestamp": time.Now().Format(time.RFC3339),
})
```
**好处**:
- 避免循环导入
- 减少依赖耦合
- ErrorHandler 可作为独立模块
---
### 3. 为什么敏感信息只在 5xx 时脱敏?
**原则**: 区分客户端错误和服务端错误
**客户端错误 (4xx)**:
- 由用户行为引起
- 可返回具体业务错误("用户名已存在"
- 不涉及内部实现细节
**服务端错误 (5xx)**:
- 由系统故障引起
- 可能暴露敏感信息(数据库结构、内部路径)
- 必须返回通用消息("内部服务器错误"
**示例**:
```go
// 客户端错误 - 保留原始消息
errors.New(CodeInvalidParam, "用户名长度必须在 3-20 个字符之间")
客户端看到: "用户名长度必须在 3-20 个字符之间"
// 服务端错误 - 脱敏
errors.Wrap(CodeDatabaseError, "查询失败", dbErr)
客户端看到: "数据库错误"
日志记录: "查询失败: connection refused at 127.0.0.1:5432"
```
---
### 4. 为什么使用两层 defer/recover
**第一层**: Recover 中间件 - 捕获业务代码 panic
```go
func Recover() fiber.Handler {
return func(c *fiber.Ctx) error {
defer func() {
if r := recover() { /* 处理 panic */ }
}()
return c.Next()
}
}
```
**第二层**: SafeErrorHandler - 防止 ErrorHandler 自身 panic
```go
func SafeErrorHandler() fiber.ErrorHandler {
return func(c *fiber.Ctx, err error) error {
defer func() {
if r := recover() { /* 降级处理 */ }
}()
return handleError(c, err)
}
}
```
**为什么需要两层**:
- ErrorHandler 在中间件之外执行
- 如果 ErrorHandler panicRecover 中间件无法捕获
- SafeErrorHandler 自我保护,确保 100% 稳定
---
## 性能优化
### 1. 错误码映射优化
**策略**: 使用 `map[int]` 而非 `switch-case`
```go
// 优化前: O(n) 时间复杂度
func GetHTTPStatus(code int) int {
switch code {
case CodeInvalidParam: return 400
case CodeMissingToken: return 401
// ... 16+ cases
}
}
// 优化后: O(1) 时间复杤度
var httpStatusMap = map[int]int{
CodeInvalidParam: 400,
CodeMissingToken: 401,
// ...
}
func GetHTTPStatus(code int) int {
if status, ok := httpStatusMap[code]; ok {
return status
}
return 500
}
```
**性能提升**: ~6 ns/op (基准测试结果)
---
### 2. 上下文提取优化
**策略**: 按需提取,避免不必要的分配
```go
// 仅在需要时提取 Query 参数
func FromFiberContext(c *fiber.Ctx) *ErrorContext {
query := ""
if c.Request().URI().QueryArgs().Len() > 0 {
query = string(c.Request().URI().QueryArgs().QueryString())
}
return &ErrorContext{
RequestID: getRequestID(c), // 使用缓存的值
Method: c.Method(),
Path: c.Path(),
Query: query,
IP: c.IP(),
UserAgent: c.Get("User-Agent"),
}
}
```
**性能指标**: ~188 ns/op, 208 B/op (基准测试结果)
---
### 3. 日志字段构造优化
**策略**: 复用 Zap 字段,减少内存分配
```go
func (ec *ErrorContext) ToLogFields() []zap.Field {
fields := make([]zap.Field, 0, 7) // 预分配容量
fields = append(fields,
zap.String("request_id", ec.RequestID),
zap.String("method", ec.Method),
zap.String("path", ec.Path),
zap.String("ip", ec.IP),
)
if ec.Query != "" {
fields = append(fields, zap.String("query", ec.Query))
}
if ec.UserID != "" {
fields = append(fields, zap.String("user_id", ec.UserID))
}
return fields
}
```
**性能指标**: ~145 ns/op, 768 B/op (基准测试结果)
---
### 4. 整体性能目标
| 指标 | 目标 | 实测 | 状态 |
|------|------|------|------|
| 错误处理延迟 (P95) | < 1ms | < 0.5μs | ✅ |
| 内存开销 | < 1KB | ~1KB | ✅ |
| 并发处理能力 | 10k+ RPS | 测试通过 | ✅ |
---
## 扩展性设计
### 1. 新增错误码
**步骤**:
1.`pkg/errors/codes.go` 添加常量:
```go
const (
CodeNewError = 1010 // 新错误码
)
```
2. 添加错误消息:
```go
var errorMessages = map[int]map[string]string{
// ...
CodeNewError: {"zh": "新错误消息"},
}
```
3. 添加 HTTP 状态码映射(如果非标准):
```go
var httpStatusMap = map[int]int{
// ...
CodeNewError: 400,
}
```
4. 添加日志级别映射(如果非标准):
```go
var logLevelMap = map[int]string{
// ...
CodeNewError: "warn",
}
```
---
### 2. 支持多语言
**扩展点**: `errorMessages` 支持多语言键
**示例**:
```go
var errorMessages = map[int]map[string]string{
CodeInvalidParam: {
"zh": "参数验证失败",
"en": "Parameter validation failed",
},
}
func GetMessage(code int, lang string) string {
if msg, ok := errorMessages[code]; ok {
if text, ok := msg[lang]; ok {
return text
}
}
return "Unknown error"
}
```
**调用**:
```go
// 从请求 Header 获取语言
lang := c.Get("Accept-Language", "zh")
message := errors.GetMessage(code, lang)
```
---
### 3. 自定义日志格式
**扩展点**: `safeLogWithLevel()` 可自定义日志结构
**示例**:
```go
func safeLogWithLevel(logger *zap.Logger, level string, msg string, fields ...zap.Field) {
// 添加自定义字段
fields = append(fields,
zap.String("service", "junhong-cmp"),
zap.String("env", os.Getenv("ENV")),
)
switch level {
case "error":
logger.Error(msg, fields...)
case "warn":
logger.Warn(msg, fields...)
default:
logger.Info(msg, fields...)
}
}
```
---
### 4. 集成监控系统
**扩展点**: 在 ErrorHandler 中添加指标上报
**示例**:
```go
func handleError(c *fiber.Ctx, err error) error {
// ... 现有逻辑 ...
// 上报错误指标
metrics.IncrementErrorCounter(code, httpStatus)
if httpStatus >= 500 {
metrics.RecordServerError(code, errCtx.Path)
}
return c.Status(httpStatus).JSON(response)
}
```
---
## 总结
### 设计亮点
1. **分层架构**: 清晰的职责划分(错误码、错误类型、上下文、处理器)
2. **防御性编程**: 双层 defer/recover 保护,确保 100% 稳定
3. **高性能**: 所有操作 < 1μs零阻塞
4. **可扩展**: 易于新增错误码、多语言、监控集成
5. **安全性**: 敏感信息脱敏,防止信息泄露
### 技术特点
- **类型安全**: 使用强类型 `AppError` 而非 `error` 字符串
- **错误链**: 支持 `errors.Unwrap()` 保留完整错误上下文
- **结构化日志**: 使用 Zap 字段而非字符串拼接
- **并发安全**: 每个请求独立处理,无共享状态
### 适用场景
- ✅ RESTful API 错误处理
- ✅ 微服务错误统一
- ✅ 高并发场景10k+ RPS
- ✅ 需要详细错误追踪的系统
---
**版本历史**:
- v1.0.0 (2025-11-15): 初始版本