← 返回博客
go2026-09-12 20:29:552 分钟 · 472 0

Gin 统一响应与错误处理:别再到处写 map

用统一 Result 结构封装成功和失败响应,写一个 Recovery 中间件把 panic 转成标准错误,再设计一套业务错误码。

每篇手写 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 组织成分层结构,项目再大也不乱。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/180