最稳妥做法是每个 handler 显式构造统一 response 结构体并调用 writejson:含 code(严格对应 http 状态码)、msg(错误提示)、data(业务数据)、pagination(仅列表接口),避免中间件自动包装导致重复写 header 或 panic。

go HTTP handler里怎么统一返回 JSON 结构
直接用 json.Marshal 拼结构体最稳妥,别依赖中间件自动包装。因为中间件无法判断 handler 是否已写入响应、是否已调用 http.Error、甚至是否 panic 了——一包装就可能重复写 header 或 panic。
推荐在每个 handler 末尾显式构造响应:
- 定义一个顶层响应结构体,比如
type Response Struct { Code int `json:"code"` Msg String `json:"msg"` Data Interface{} `json:"data,omitempty"` } - handler 内部逻辑完成后再调用
json.NewEncoder(w).Encode(resp),不提前w.WriteHeader - 错误路径也走同一结构体:比如
Response{Code: 400, Msg: "invalid id"},而不是混用http.Error
为什么不能把 error 和 data 放进同一个字段
前端同学会疯。当 Data 是 nil 时,Msg 承载错误描述;但若后端误把错误信息塞进 Data(比如 Data: map[string]string{"error": "timeout"}),前端就得写两套解析逻辑。
更麻烦的是 Swagger 文档和 typescript 类型推导——字段语义混乱会让自动生成的 client 代码不可靠。
立即学习“go语言免费学习笔记(深入)”;
-
Code严格对应 HTTP 状态码语义(如 200/400/500),不用于业务码 - 业务状态放
Code字段是常见坑,比如返回Code: 1001却设http.StatusOK,nginx 或 CDN 可能缓存失败 - 真正需要业务码?加个
BusinessCode int `json:"biz_code,omitempty"`字段,和 HTTP 状态解耦
要不要支持分页元信息嵌套在 response.data 里
不要。分页不是业务数据的一部分,强行塞进 Data 会让前端每次都要做类型断言或判空,TypeScript 接口得写成 Data: T | Paginated<t></t>,徒增复杂度。
统一做法:顶层加 Pagination *PaginationInfo `json:"pagination,omitempty"` 字段,仅列表接口返回它。
-
PaginationInfo包含Total、Page、PageSize、Pages即可,别加HasNext这类计算字段(前端自己算更准) - 非列表接口(如
GET /user/{id})不返回Pagination字段,避免误导 - 注意 JSON 序列化时
omitempty生效前提:字段必须是指针或可为空类型,int类型的Total得改成*int或用json.number
gorilla/mux 或 chi 中间件统一包装的致命问题
中间件能拦截 http.Handler,但拦不住 panic,也拦不住 handler 里直接 os.Exit 或 log.Fatal。一旦出错,包装逻辑跳过,返回就是裸的 500 页面或空响应。
更隐蔽的问题:有些 handler 显式调用了 w.WriteHeader(204) 或 w.WriteHeader(302),再走 JSON 封装就会触发 http: multiple response.WriteHeader calls panic。
- 真要用中间件,只做日志、CORS、超时控制等无副作用操作
- JSON 封装逻辑必须收口到一个函数,比如
WriteJSON(w http.ResponseWriter, status int, v interface{}),并在所有 handler 中显式调用 - 配合
recover()的 defer 函数处理 panic,但注意:recover 不捕获 runtime error(如 nil pointer dereference),仍会 crash
事情说清了就结束。最复杂的点其实是团队对「谁负责写 HTTP 状态码」没共识——有人觉得 handler 写,有人觉得封装层写,结果 code 字段含义漂移,debug 时翻三天日志。