第16章:统一响应封装
本章分析 internal/pkg/response------只有 89 行却定义了全 API 的对外契约:{code, message, data} 信封、语义化错误构造函数、分页结构。前端 src/services/http.ts 与 businessCodes.ts 逐字段对齐这个信封(第 48 章),它是前后端协作的宪法。
流程
所有 handler 的响应出口都收敛到 response 包。成功路径:service 返回业务数据 → handler 调 response.Success(c, data) → 输出 {"code":0,"message":"success","data":...}。失败路径:handler 判定错误类别 → 调对应语义函数(BadRequest/NotFound/Conflict/Unauthorized/Forbidden/InternalError)→ 同时设置 HTTP 状态码与业务 code。
信封里存在两层编码:HTTP status (网关、缓存、通用客户端关心)与 body.code (前端业务逻辑关心)。规则很朴素:400/401/403/404/409/500 各自对应同名函数,业务 code 与 HTTP 码取相同数值(Fail(c, http.StatusBadRequest, 400, msg)),唯一的例外是 Conflict------409 的 HTTP 状态可携带自定义业务 code(默认 409),用于「重复创建」「状态冲突」等需要前端精确分支的场景(FailWithData 还能在失败时附 data,如提示哪条记录冲突)。
分页统一为 PageData{list, total, page, page_size},所有列表接口(素材、项目、任务、提示词)共用,前端表格组件因此只需一种分页模型。
实现
设计上的几个要点:
Data interface{} + omitempty:成功必有 data(可为 null 表示无返回体),失败时省略 data 字段而不是返回 null------前端判空逻辑统一。- code=0 表示成功 :与「0 是零值」的直觉一致,前端
code !== 0一条判断即可分流;错误码目前与 HTTP 码同值,未来分化业务码(如 1xxxx 系列)不需要改信封结构。 - 函数语义化而非参数化 :handler 里写
response.BadRequest(c, "名称不能为空")而非response.Fail(c, 400, 400, "...")------错误类别在调用点即读即懂,防止随手传 500 掩盖真实错误类别。 - Message 全部中文业务描述:错误信息直接面向最终用户(前端 toast 直接展示原文),因此书写规范是「可展示的中文短句」而非英文技术串。这与很多「错误码+前端查表翻译」的方案不同,选择了零翻译直通------简单,代价是国际化需要时全量改造。
- 包零依赖 (只 import gin 与 net/http):可被 middleware、handler、任何层引用而不引入循环依赖;
response_test.go用 httptest 验证 JSON 序列化形态。
值得注意的是鉴权失败路径:middleware/auth.go 里 response.Unauthorized(c, "Token 无效或已过期") + c.Abort()------中间件层同样走统一信封,前端拦截器因此能用同一套逻辑识别会话过期(第 48 章的 isSessionExpiredCode 恰好检查 code 401 / 12010)。
📌 设计决策
- 双层编码(HTTP + body code)而非仅 HTTP:浏览器/代理只认 HTTP;前端业务分支需要稳定语义码,两者各司其职。
- 中文 message 直通 UI:省一张错误码翻译表;适合单语产品,多语言需引入 message key。
- 分页三件套(page/page_size/total)不带排序参数:排序固定在仓储层(如任务固定 created_at DESC),减少 API 面积。
代码示例
信封与成功/失败构造的完整形态:
go
// internal/pkg/response/response.go(节选)
// Body 统一响应结构体。
type Body struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Body{Code: 0, Message: "success", Data: data})
}
func Fail(c *gin.Context, httpStatus, code int, message string) {
c.JSON(httpStatus, Body{Code: code, Message: message})
}
// Conflict 返回 409 冲突错误(业务 code 可自定义)。
func Conflict(c *gin.Context, code int, message string, data interface{}) {
FailWithData(c, http.StatusConflict, code, message, data)
}
分页结构与典型 handler 用法:
go
// PageData 分页数据结构。
type PageData struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// handler 中的典型收尾(示意)
items, total, err := svc.List(ctx, filter, page, pageSize)
if err != nil {
response.InternalError(c, "查询任务列表失败")
return
}
response.Success(c, response.PageData{
List: items, Total: total, Page: page, PageSize: pageSize,
})
小结
{code, message, data}信封 + 语义化错误函数 + PageData 分页 = API 对外契约三件套。- HTTP 码与业务码同值、code=0 成功、中文 message 直通前端 toast。
- 中间件与 handler 共用信封,前端会话过期识别因此统一。
思考题
- 若引入错误码分级(参数错 4xxxx / 业务冲突 5xxxx),Confict 的自定义 code 通道应如何扩展成通用机制?
- Data 的 omitempty 在 Go 空切片时会把 data 序列化成 null 还是省略?如何保证前端拿到 \[\]?
项目信息
- GitHub仓库:github.com/Chyona/live-mixer
- 项目案例:gogoshine.com