统一响应与错误处理:封装与中间件

本章解决什么问题:接口一多,每个 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。

笔记加载中…