第 7 章 · 请求校验与统一响应
本章目标:使用 go-playground/validator 与 Gin binding 做请求校验;定义统一 JSON envelope {code, message, data};规范化数字错误码与 HTTP 状态映射,40301 无权限等与 flask-web ch07、fastapi-web ch12 对齐;重构 ch05/ch06 handler 全面使用 response 包。
学时建议:3~4 小时(含 1.5 小时跟练)
前置:完成 gin-web ch06。
7.1 场景说明:一致的前端契约
user-demo SPA 与 api-demo 前端共用 axios 拦截器,期望所有 API 返回一致结构:
{
"code": 0,
"message": "success",
"data": { "items": [], "total": 0 }
}
错误示例:
{
"code": 42201,
"message": "slug is required",
"data": null
}
无权限(与 flask-web 对齐):
{
"code": 40301,
"message": "无权限",
"data": null
}
internal/
├── response/
│ ├── envelope.go
│ └── codes.go
└── handler/
└── bind.go
对照章节:
| 主题 | flask-web ch07 | fastapi-web ch12 | gin-web ch07 |
|---|---|---|---|
| 成功码 | 0 | 自定义 | 0 |
| 无权限 | 40301 | 40301 | 40301 |
| 校验失败 | 42201 | 422 | 42201 |
| 校验引擎 | marshmallow | Pydantic | binding + validator |
7.2 逐步操作表
| 步骤 | 操作 | 验证 |
|---|---|---|
| 1 | 创建 response/envelope.go、codes.go | 包编译 |
| 2 | 创建 handler/bind.go 统一绑定 | 校验错误友好 |
| 3 | 重构 ProductHandler 使用 response | 成功含 code=0 |
| 4 | 重构 AuthHandler / middleware | 40301 一致 |
| 5 | 注册自定义 validator slug | 非法 slug 42201 |
| 6 | curl 成功列表 | data 含 items |
| 7 | curl 非法 body | 42201 |
| 8 | user 写商品 | 40301 |
7.3 Envelope 定义
// internal/response/envelope.go
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, Envelope{Code: CodeOK, Message: "success", Data: data})
}
func Created(c *gin.Context, data any) {
c.JSON(http.StatusCreated, Envelope{Code: CodeCreated, Message: "created", Data: data})
}
func Fail(c *gin.Context, httpStatus, code int, msg string) {
c.JSON(httpStatus, Envelope{Code: code, Message: msg, Data: nil})
}
func NoContent(c *gin.Context) {
c.Status(http.StatusNoContent)
}
7.4 错误码表(对齐 flask-web ch07)
// internal/response/codes.go
package response
const (
CodeOK = 0
CodeCreated = 0 // 创建成功仍用 0,HTTP 201 区分
CodeUnauthorized = 40101
CodeForbidden = 40301 // 与 flask-web 一致
CodeNotFound = 40400
CodeConflict = 40901
CodeValidation = 42201
CodeInternal = 50000
)
| code | HTTP | 场景 | flask-web | api-go-demo |
|---|---|---|---|---|
| 0 | 200/201 | 成功 | 0 | 0 |
| 40101 | 401 | 未登录/无效 token | 40101 | 40101 |
| 40301 | 403 | 无权限 | 40301 | 40301 |
| 40400 | 404 | slug 不存在 | 40400 | 40400 |
| 40901 | 409 | slug 重复 | — | 40901 |
| 42201 | 400/422 | 参数校验失败 | 42201 | 42201 |
| 50000 | 500 | 服务器错误 | 50000 | 50000 |
Gin 校验失败常用 HTTP 400;为与 flask 数字码一致,body 中 code 仍为 42201。
7.5 统一绑定 helper
// internal/handler/bind.go
package handler
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"example.com/api-go-demo/internal/response"
)
func BindJSON(c *gin.Context, dst any) bool {
if err := c.ShouldBindJSON(dst); err != nil {
msg := formatBindError(err)
response.Fail(c, http.StatusBadRequest, response.CodeValidation, msg)
return false
}
return true
}
func BindQuery(c *gin.Context, dst any) bool {
if err := c.ShouldBindQuery(dst); err != nil {
msg := formatBindError(err)
response.Fail(c, http.StatusBadRequest, response.CodeValidation, msg)
return false
}
return true
}
func formatBindError(err error) string {
if ve, ok := err.(validator.ValidationErrors); ok && len(ve) > 0 {
fe := ve[0]
field := strings.ToLower(fe.Field())
switch fe.Tag() {
case "required":
return fmt.Sprintf("%s is required", field)
case "email":
return "invalid email format"
case "min":
return fmt.Sprintf("%s is too short", field)
case "max":
return fmt.Sprintf("%s is too long", field)
case "gte":
return fmt.Sprintf("%s must be >= %s", field, fe.Param())
case "slug":
return "slug must match [a-z0-9-]+"
default:
return fe.Error()
}
}
return err.Error()
}