统一响应与错误处理:封装与中间件
本章解决什么问题:接口一多,每个 handler 各写各的响应结构,前端解析成本高、错误口径乱。本章把“成功/失败”收口成统一 JSON 外壳,并用中间件兜住未处理错误。
统一响应结构
约定一个外壳:业务码 code(0 表示成功)、消息 msg、数据 data。所有 handler 只负责往外壳里填内容,不再各写各的:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Code int `json:"code"` // 业务码,0 表示成功
Msg string `json:"msg"`
Data any `json:"data"`
}
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, Response{Code: 0, Msg: "ok", Data: data})
}
func Fail(c *gin.Context, httpStatus, code int, msg string) {
c.JSON(httpStatus, Response{Code: code, Msg: msg})
}
func main() {
r := gin.Default()
r.GET("/user/:id", func(c *gin.Context) {
OK(c, gin.H{"id": c.Param("id")})
})
r.Run(":8080")
}
说明:HTTP 状态码用于表达传输层结果(200/400/500),业务码用于表达业务层结果,两者可同时存在。前端先看 HTTP 状态、再读 code 分支业务逻辑。
兜底:NoRoute 与错误中间件
没有被任何路由接住的请求统一回外壳而不是 Gin 默认的纯文本:
r.NoRoute(func(c *gin.Context) {
Fail(c, http.StatusNotFound, 40400, "no such route")
})
业务代码里“想报错又不想层层 return”时,可以用 c.Error(err) 把错误挂到请求上,再由一个错误收尾中间件统一处理——链条执行完、响应还没定型时检查 c.Errors:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
Fail(c, http.StatusInternalServerError, 50000, err.Error())
}
}
}
需要同时兜住 panic 时,把第 13 章的自写恢复中间件挂上,并在恢复分支里也输出统一外壳,前后端口径就完全一致了。
错误码规划建议
- 按模块分段编号,如 404xx 路由类、400xx 参数类、500xx 服务类,便于日志检索;
- 错误码表单独成文档,msg 面向用户可读,详细原因放日志或 data 字段;
- handler 内错误优先“就地处理并 return”,只有横切性错误才交给中间件,避免滥用。
关键点
- 统一外壳 = 业务码 + 消息 + 数据,成功与失败都走同一结构。
- NoRoute 让 404 也保持 JSON;错误中间件收拢 c.Errors 与 panic 两条错误路径。
- HTTP 状态码与业务码分工不同,别用业务码代替 HTTP 状态。
小结
统一响应把“怎么回”收敛到两个小函数,错误中间件把“没处理的错”关进笼子。下一章把配置从硬编码里解放出来:环境变量加 viper。