最稳妥方式是每个handler显式构造response结构体并调用json.newencoder(w).encode(),避免中间件自动包装;response需含code、msg、data、timestamp字段,http状态码与业务码解耦,所有分支统一走同一响应路径。

Go 语言做接口响应统一格式,最稳妥的方式是**每个 handler 显式构造结构体 + json.NewEncoder(w).Encode()**,而不是依赖中间件自动包装。中间件在 panic、提前 http.Error 或多次 WriteHeader 时极易出错,反而增加维护成本。
定义 Response 结构体要注意字段导出和 JSON tag
结构体字段必须首字母大写才能被 json.Marshal 序列化;json tag 缺一不可,且不能拼错:
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
Timestamp int64 `json:"timestamp"`
}
-
Timestamp用int64(如time.Now().UnixMilli()),前端直接Date.now()对齐,不传time.Time避免时区和格式歧义 -
Data用interface{}是为了泛型兼容,但实际传入应是具体 struct(如User{}),不是map[string]interface{}—— 否则 Swagger 和 TypeScript 类型推导失效 - 别加
Pagination字段到顶层:它只属于列表接口的Data内部(如Data: struct{ List []User; Pagination Pagination }{})
handler 里别用 Success/Fail 工具函数封装业务语义
Success()、Fail() 这类方法容易把业务逻辑(比如 “登录失败” vs “参数错误”)和协议层(HTTP 状态码、JSON 序列化)混在一起。它们不该决定 Code 是 0 还是 200,也不该内置 "success" 字符串。
- 真正该封装的只有三件事:
w.Header().Set("Content-Type", "application/json")、json.NewEncoder(w).Encode(resp)、w.WriteHeader(statusCode) - 业务码和 HTTP 状态码要解耦:HTTP 状态码走
w.WriteHeader()(如http.StatusOK、http.StatusBadRequest),业务码走Response.Code字段(如1001表示“用户不存在”) - 如果真要简化,只封装序列化动作,例如:
func WriteJSON(w http.ResponseWriter, status int, v interface{}) { w.WriteHeader(status); json.NewEncoder(w).Encode(v) }
为什么不要用中间件自动包装返回体
中间件拦截 Write 的方式看似省事,但实际踩坑率极高:
- handler 里调了
http.Error(w, "...", 400),中间件仍尝试读取空 body 并 marshal,结果返回{"code":200,"msg":"success","data":null}—— 错误被吞掉 - handler panic 了,中间件还没来得及 wrap 就已崩溃,日志里看不到原始错误上下文
- 多个中间件叠加时,header 可能被重复设置,触发
http: superfluous response.WriteHeader callpanic - 无法判断 handler 是否已调用
w.WriteHeader,强行覆盖会破坏 HTTP 流程(比如流式响应、文件下载)
真正难的不是写一个通用结构体,而是坚持让每个 handler 在末尾显式构造 Response 并调用一次 WriteJSON —— 所有分支(包括 error 分支)都走同一路径,才能保证前端永远拿到可预测的 shape。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











