每篇手写 c.JSON(200, gin.H{...}) 有两个问题:前端解析字段不固定,错误散落在各 handler 里难以统一。这一篇把响应格式和错误处理收口到一个地方。
1. 定义统一返回结构
约定一个所有接口都返回的包络:
type Result struct { Code int `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"` }
code 用 0 表示成功,非零表示各类错误;data 在失败时放 nil。建一个 response.go 放 helper:
func OK(c *gin.Context, data interface{}) { c.JSON(200, Result{Code: 0, Msg: "ok", Data: data}) } func Fail(c *gin.Context, code int, msg string) { c.JSON(200, Result{Code: code, Msg: msg, Data: nil}) }
为什么成功也统一返回 200?HTTP 状态码只表达传输层成败,业务成败放进 code。前端永远读 code 判断,不用同时看 HTTP 状态和业务码两套逻辑。这样 Android、iOS、Web 三端解析完全一致。
2. 业务错误码
定义一个错误码常量段,避免魔法数字:
const ( CodeOK = 0 CodeParamErr = 40000 CodeUnauthorized = 40100 CodeForbidden = 40300 CodeNotFound = 40400 CodeServerErr = 50000 ) var msgOf = map[int]string{ CodeParamErr: "参数错误", CodeUnauthorized: "未登录或登录失效", CodeForbidden: "无权限", CodeNotFound: "资源不存在", CodeServerErr: "服务内部错误", }
handler 里直接调:
func getArticle(c *gin.Context) { id := c.Param("id") if id == "" { Fail(c, CodeParamErr, msgOf[CodeParamErr]) return } // 查库 OK(c, gin.H{"id": id}) }
3. 用 Recovery 兜住 panic
没兜住的 panic 会让 Gin 默认 Recovery 返回 500 且带堆栈,暴露内部信息。自己写一个,把任何 panic 转成统一错误:
func Recovery() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { log.Printf("panic: %v", err) // 记日志,别回传细节 Fail(c, CodeServerErr, msgOf[CodeServerErr]) c.Abort() } }() c.Next() } }
挂到全局最前面:
r := gin.New() r.Use(Recovery()) // 必须第一个,保证后续 panic 都能兜 r.Use(gin.Logger())
4. 业务错误也走统一出口
有些错误来自底层(数据库、RPC),建议封装成带码的 error,handler 只管转发:
type BizError struct { Code int Msg string } func (e *BizError) Error() string { return e.Msg } func FailErr(c *gin.Context, err error) { if be, ok := err.(*BizError); ok { Fail(c, be.Code, be.Msg) return } Fail(c, CodeServerErr, msgOf[CodeServerErr]) }
service 层返回 &BizError{Code: CodeNotFound, Msg: "文章不存在"},handler 一行 FailErr(c, err) 就完事,不用每个地方判断错误类型。
5. 校验错误直接复用
第 2 篇的 ShouldBindJSON 错误接进来:
func createArticle(c *gin.Context) { var req Article if err := c.ShouldBindJSON(&req); err != nil { Fail(c, CodeParamErr, err.Error()) return } OK(c, req) }
err.Error() 已经带字段名和规则,前端能直接提示用户哪填错了。
6. 一个标准 handler 长这样
func getProfile(c *gin.Context) { uid, _ := c.Get("uid") user, err := userService.Get(uid.(uint)) if err != nil { FailErr(c, err) return } OK(c, user) }
成功 OK、失败 FailErr,没有散落的 gin.H。响应格式和错误出口都收口在 response.go,后面加接口不再操心这两件事。
下一篇把路由和 handler 组织成分层结构,项目再大也不乱。