- 新增统一错误码定义和管理 (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
13 KiB
Quick Start: Fiber 错误处理集成
Feature: 003-error-handling
Date: 2025-11-14
Audience: 新开发者、集成工程师
概述
本文档提供 Fiber 错误处理集成的快速上手指南,帮助开发者快速理解和使用统一的错误处理机制。
5 分钟快速开始
1. 错误响应格式
所有 API 错误响应都使用统一格式:
{
"code": 1001,
"data": null,
"msg": "参数验证失败",
"timestamp": "2025-11-14T16:00:00+08:00"
}
code: 错误码 (1000-1999: 客户端错误, 2000-2999: 服务端错误)data: 错误时始终为nullmsg: 用户友好的错误消息 (中文)timestamp: ISO 8601 格式时间戳
Request ID 在响应 Header 中:
X-Request-ID: f1d8b767-dfb3-4588-9fa0-8a97e5337184
2. 在 Handler 中返回错误
方式 1: 使用预定义错误码
import (
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
func (h *Handler) CreateUser(c *fiber.Ctx) error {
var req CreateUserRequest
if err := c.BodyParser(&req); err != nil {
// 返回参数验证失败错误
return errors.New(errors.CodeInvalidParam, "参数格式错误")
}
// 业务逻辑...
user, err := h.service.Create(req)
if err != nil {
// 包装错误,添加错误码
return errors.Wrap(errors.CodeDatabaseError, "创建用户失败", err)
}
return response.Success(c, user)
}
方式 2: 直接返回 error (由 ErrorHandler 自动处理)
func (h *Handler) GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(c.Context(), id)
if err != nil {
// 直接返回错误,ErrorHandler 会自动映射为 500 错误
return err
}
return response.Success(c, user)
}
方式 3: 使用预定义错误常量
import "github.com/break/junhong_cmp_fiber/pkg/errors"
func (m *Middleware) CheckAuth(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
// 使用预定义错误
return errors.ErrMissingToken
}
return c.Next()
}
3. 常用错误码
| 错误码 | HTTP 状态 | 消息 | 使用场景 |
|---|---|---|---|
| 1001 | 400 | 参数验证失败 | 请求参数不符合规则 |
| 1002 | 401 | 缺失认证令牌 | 未提供 Authorization header |
| 1003 | 401 | 无效或过期的令牌 | 令牌验证失败 |
| 1006 | 404 | 资源未找到 | 数据库中找不到资源 |
| 1008 | 429 | 请求过多 | 触发限流 |
| 2001 | 500 | 内部服务器错误 | 未预期的服务器错误 |
| 2002 | 500 | 数据库错误 | 数据库操作失败 |
| 2003 | 500 | 缓存服务错误 | Redis 操作失败 |
完整列表: 见 pkg/errors/codes.go
4. 客户端错误处理示例
JavaScript/TypeScript:
async function getUser(userId: string): Promise<User> {
const response = await fetch(`/api/v1/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.code !== 0) {
// 错误处理
const requestId = response.headers.get('X-Request-ID');
switch (data.code) {
case 1002:
case 1003:
// 认证失败,跳转登录
redirectToLogin();
break;
case 1006:
// 资源未找到
showNotFoundMessage();
break;
case 2001:
case 2002:
// 服务器错误,提示用户联系技术支持
showErrorMessage(`服务器错误,请联系技术支持\nRequest ID: ${requestId}`);
break;
default:
showErrorMessage(data.msg);
}
throw new Error(data.msg);
}
return data.data;
}
Python:
import requests
def get_user(user_id: str, token: str) -> dict:
response = requests.get(
f'/api/v1/users/{user_id}',
headers={'Authorization': f'Bearer {token}'}
)
data = response.json()
request_id = response.headers.get('X-Request-ID')
if data['code'] != 0:
# 错误处理
if data['code'] in [1002, 1003]:
raise AuthenticationError(data['msg'])
elif data['code'] == 1006:
raise NotFoundError(data['msg'])
elif data['code'] >= 2000:
raise ServerError(f"{data['msg']} (Request ID: {request_id})")
else:
raise APIError(data['msg'])
return data['data']
进阶使用
自定义错误消息
// 使用预定义错误码 + 自定义消息
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(c.Context(), id)
if err != nil {
// 自定义错误消息
return errors.New(errors.CodeNotFound, fmt.Sprintf("用户 %s 不存在", id))
}
// 更新逻辑...
}
错误链传递
// Service 层
func (s *Service) CreateOrder(req *CreateOrderRequest) (*Order, error) {
user, err := s.userService.GetByID(req.UserID)
if err != nil {
// 包装错误,保留错误链
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 订单创建逻辑...
}
// Handler 层
func (h *Handler) CreateOrder(c *fiber.Ctx) error {
order, err := h.service.CreateOrder(req)
if err != nil {
// 包装为 AppError,原始错误链会记录到日志
return errors.Wrap(errors.CodeInternalError, "创建订单失败", err)
}
return response.Success(c, order)
}
Panic 自动恢复
Panic 会被自动捕获并转换为 500 错误:
func (h *Handler) DangerousOperation(c *fiber.Ctx) error {
// 如果这里发生 panic
result := riskyFunction()
// Recover 中间件会捕获 panic,返回统一错误响应
// 客户端收到: {"code": 2001, "msg": "内部服务器错误"}
// 完整堆栈会记录到日志
return response.Success(c, result)
}
注意: 不要滥用 panic,业务错误应该使用 error 返回。
调试技巧
1. 使用 Request ID 追踪错误
客户端获取 Request ID:
curl -i http://localhost:8080/api/v1/users/123
响应:
HTTP/1.1 500 Internal Server Error
X-Request-ID: f1d8b767-dfb3-4588-9fa0-8a97e5337184
Content-Type: application/json
{"code": 2002, "data": null, "msg": "数据库错误", "timestamp": "..."}
在日志中搜索 Request ID:
grep "f1d8b767-dfb3-4588-9fa0-8a97e5337184" logs/app.log
日志会包含完整的错误详情:
{
"level": "error",
"timestamp": "2025-11-14T16:00:00+08:00",
"request_id": "f1d8b767-dfb3-4588-9fa0-8a97e5337184",
"method": "GET",
"path": "/api/v1/users/123",
"error": "pq: relation 'users' does not exist",
"stack": "..."
}
2. 本地开发时查看完整错误
开发环境: 查看 logs/app.log 获取详细错误信息
生产环境: 使用 Request ID 联系运维团队查看日志
常见错误场景
场景 1: 参数验证失败
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
}
func (h *Handler) CreateUser(c *fiber.Ctx) error {
var req CreateUserRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求格式错误")
}
// 使用 validator 验证
if err := validate.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "参数验证失败")
}
// 创建用户...
}
场景 2: 资源未找到
func (h *Handler) GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.service.GetByID(c.Context(), id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(errors.CodeNotFound, "用户不存在")
}
return errors.Wrap(errors.CodeDatabaseError, "查询用户失败", err)
}
return response.Success(c, user)
}
场景 3: 数据库错误
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
id := c.Params("id")
var req UpdateUserRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求格式错误")
}
user, err := h.service.Update(c.Context(), id, &req)
if err != nil {
// 数据库错误会被包装,原始错误仅记录到日志
return errors.Wrap(errors.CodeDatabaseError, "更新用户失败", err)
}
return response.Success(c, user)
}
场景 4: 外部服务不可用
func (h *Handler) SendEmail(c *fiber.Ctx) error {
var req SendEmailRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求格式错误")
}
err := h.emailService.Send(req.To, req.Subject, req.Body)
if err != nil {
// 外部服务错误
return errors.Wrap(errors.CodeServiceUnavailable, "邮件服务暂时不可用", err)
}
return response.Success(c, nil)
}
最佳实践
✅ 推荐做法
-
使用预定义错误码: 保持错误码的一致性
return errors.New(errors.CodeInvalidParam, "用户名不能为空") -
包装底层错误: 保留错误链,便于调试
return errors.Wrap(errors.CodeDatabaseError, "查询失败", err) -
提供友好的错误消息: 使用中文,面向用户
return errors.New(errors.CodeNotFound, "订单不存在") -
区分客户端和服务端错误: 使用正确的错误码范围
- 1000-1999: 客户端问题 (参数错误、权限不足等)
- 2000-2999: 服务端问题 (数据库错误、服务不可用等)
❌ 避免做法
-
不要硬编码错误消息
// ❌ 错误 return c.Status(400).JSON(fiber.Map{"error": "参数错误"}) // ✅ 正确 return errors.New(errors.CodeInvalidParam, "参数错误") -
不要暴露敏感信息
// ❌ 错误: 暴露 SQL 语句 return errors.New(errors.CodeDatabaseError, err.Error()) // ✅ 正确: 使用通用消息 return errors.Wrap(errors.CodeDatabaseError, "查询失败", err) -
不要滥用 panic
// ❌ 错误: 业务错误不应该 panic if user == nil { panic("user not found") } // ✅ 正确: 使用 error 返回 if user == nil { return errors.New(errors.CodeNotFound, "用户不存在") } -
不要忽略错误
// ❌ 错误: 忽略错误 user, _ := h.service.GetByID(id) // ✅ 正确: 处理错误 user, err := h.service.GetByID(id) if err != nil { return errors.Wrap(errors.CodeDatabaseError, "获取用户失败", err) }
测试错误处理
单元测试示例
func TestHandler_CreateUser_InvalidParam(t *testing.T) {
app := fiber.New()
handler := NewHandler(mockService, logger)
app.Post("/users", handler.CreateUser)
// 发送无效请求
req := httptest.NewRequest("POST", "/users", strings.NewReader(`{"username": ""}`))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
// 验证状态码
assert.Equal(t, 400, resp.StatusCode)
// 验证响应格式
var result response.Response
json.NewDecoder(resp.Body).Decode(&result)
assert.Equal(t, errors.CodeInvalidParam, result.Code)
assert.Nil(t, result.Data)
assert.NotEmpty(t, result.Message)
// 验证 Request ID header
requestID := resp.Header.Get("X-Request-ID")
assert.NotEmpty(t, requestID)
}
常见问题 (FAQ)
Q: 为什么错误响应中没有 request_id 字段?
A: Request ID 在响应 Header X-Request-ID 中传递,不在响应体中。这符合 HTTP 标准实践。
Q: 如何添加新的错误码?
A: 在 pkg/errors/codes.go 中添加常量定义和错误消息映射:
const (
CodeMyNewError = 1010 // 客户端错误
)
var errorMessages = map[int]string{
CodeMyNewError: "我的新错误",
}
Q: 服务端错误为什么只返回通用消息?
A: 出于安全考虑,避免泄露数据库结构、文件路径等敏感信息。完整错误详情会记录到日志,运维团队可以通过 Request ID 查看。
Q: Panic 会导致服务崩溃吗?
A: 不会。Recover 中间件会捕获所有 panic,转换为 500 错误响应,确保服务继续运行。
Q: 如何在日志中搜索特定用户的所有错误?
A: 日志包含 user_id 字段 (如果已认证),可以搜索:
grep '"user_id":"123"' logs/app.log | grep '"level":"error"'
下一步
- 查看完整的错误码列表:
pkg/errors/codes.go - 了解错误处理实现细节:
specs/003-error-handling/research.md - 查看 API contracts:
specs/003-error-handling/contracts/error-responses.yaml - 阅读完整实施计划:
specs/003-error-handling/plan.md
版本: 1.0.0
最后更新: 2025-11-14