不能用中间件统一写响应,因中间件执行在handler之前,无法获知响应数据、状态码及错误类型,且fiber的ctx为一次性写入模型,提前调用c.json()等会导致后续响应panic;统一格式必须由handler内主动调用封装方法(如c.success())实现。

直接封装 fiber.Ctx 的响应方法就能实现统一格式,不需要额外中间件或全局钩子——中间件只适合做前置逻辑,真正影响响应体结构的必须在 handler 内部或通过包装 Ctx 实现。
为什么不能用中间件统一写响应
中间件执行在 handler 之前,它无法知道 handler 最终要返回什么数据、状态码或错误类型;强行在中间件里 c.SendString() 或 c.JSON() 会导致后续 handler 再调用响应方法时 panic:cannot set header after written。Fiber 的 fiber.Ctx 是一次性写入模型,底层已 flush 就不能再改 header 或 body。
- 中间件适合做鉴权、日志、CORS,不适合干预响应体结构
- 统一响应必须由 handler 主动调用封装好的方法(如
c.Success(data)) - 所有错误路径也得走同一套出口,否则 400/500 响应格式会不一致
推荐:扩展 fiber.Ctx 接口封装响应方法
利用 Fiber v3 的 NewWithCustomCtx 创建带业务方法的上下文类型,比写工具函数更安全——方法绑定在实例上,不会误传 ctx 或漏掉 status。
- 定义新结构体嵌入
fiber.Ctx,添加Success()、Fail()、ValidationFailed()等方法 - 每个方法内部统一设置
Content-Type: application/json和标准字段(如code、message、data) - 避免在方法里直接调
c.Status().JSON(),改用c.SendStatus()+c.SendString()组合,防止被中间件干扰
示例:
type RespCtx struct {
fiber.Ctx
}
func (c *RespCtx) Success(data interface{}) error {
return c.Status(fiber.StatusOK).JSON(map[string]interface{}{
"code": 0,
"message": "success",
"data": data,
})
}
func (c *RespCtx) Fail(code int, msg string) error {
return c.Status(code).JSON(map[string]interface{}{
"code": code,
"message": msg,
"data": nil,
})
}
错误处理必须收敛到同一出口
HTTP 错误(如参数校验失败)、业务错误(如用户不存在)、panic 都要转成统一响应格式,否则前端要写三套解析逻辑。
- 用
defer func() { if r := recover(); r != nil { c.Fail(500, "internal error") } }()捕获 panic - 校验失败不要直接
c.Status(400).SendString(),统一走c.Fail(400, "...") - 数据库查不到记录时,别返回空 JSON,而是
c.Fail(404, "user not found") - 避免在 service 层 new error 并原样抛给 handler,应在 handler 内判断 err 类型再映射成响应码
注意 JSON 序列化细节和性能陷阱
Fiber 默认用 encoding/json,但如果你的 data 字段含 time.Time、sql.NullString 或自定义类型,没加 MarshalJSON 方法就会序列化出空值或 panic。
- 所有 DTO 结构体字段必须加
json:tag,且首字母大写(否则不可导出) - 时间字段优先用
time.Time+ 自定义 marshaler,而不是字符串拼接 - 避免在
Success()里传指针或未初始化 map/slice,会导致null而不是{}或[] - 高频接口可预分配 map 容量(如
make(map[string]interface{}, 3)),减少 GC 压力
最易被忽略的一点:Fiber 的 JSON() 方法默认不格式化,也不压缩,如果要兼容调试需求,得自己加 json.MarshalIndent ——但生产环境务必关掉,否则性能下降明显。











