请求绑定与校验

手写 c.Query("name") 一两个参数还好,几十个表单字段写起来很痛苦。

Gin 的绑定(Bind)机制,能把查询串、表单、JSON Body 自动映射到 struct,并触发校验。


1.1 绑定的本质

源码位置 :binding/binding.go:30-91

Binding 接口:

go 复制代码
type Binding interface {
    Name() string
    Bind(*http.Request, any) error
}

Gin 内置了 14 种 Binding,根据 Content-Type 自动选择:

变量 对应 Content-Type 来源
binding.JSON application/json Body
binding.XML application/xml / text/xml Body
binding.YAML application/x-yaml Body
binding.TOML application/toml Body
binding.Form application/x-www-form-urlencoded Body + URL
binding.Query - URL query
binding.Uri - 路径参数
binding.Header - 请求头
binding.FormMultipart multipart/form-data Body
binding.ProtoBuf application/x-protobuf Body
binding.MsgPack application/x-msgpack Body
binding.Plain text/plain Body
binding.BSON application/bson Body

binding.Default(method, contentType) 根据 Content-Type 自动返回合适的 Binding。


1.2 两种绑定 API:Should* vs Bind*

强烈推荐:始终用 Should* 系列

API 失败行为 推荐
c.ShouldBind / ShouldBindJSON / ... 仅返回 error,需自己处理
c.Bind / BindJSON / ... 自动写 400 响应,不灵活

⚠️新手陷阱 :c.Bind 失败时自动 c.AbortWithStatus(400),

但响应格式是 Gin 默认的 {"error": ...},不符合你团队约定的 JSON 结构。

go 复制代码
// ❌ 不推荐
if err := c.Bind(&req); err != nil {
    // Bind 已经自动写了 400,这里再 c.JSON 会出错或被忽略
    return
}

// ✅ 推荐
if err := c.ShouldBindJSON(&req); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": err.Error()})
    return
}

1.3 按来源绑定

1.3.1 JSON Body

go 复制代码
type LoginReq struct {
    Username string `json:"username" binding:"required,min=3,max=32"`
    Password string `json:"password" binding:"required,min=6"`
}

r.POST("/login", func(c *gin.Context) {
    var req LoginReq
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }
    c.JSON(200, gin.H{"user": req.Username})
})

请求:

json 复制代码
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret1"}'

1.3.2 查询参数

go 复制代码
type ListReq struct {
    Page int    `form:"page" binding:"min=1"`
    Size int    `form:"size" binding:"min=1,max=100"`
    Q    string `form:"q"`
}

r.GET("/users", func(c *gin.Context) {
    var req ListReq
    if err := c.ShouldBindQuery(&req); err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }
    // ... 使用 req.Page / req.Size
})

📌关键 tag :JSON 用 json:"name",Form/Query 用 form:"name",

URI 参数用 uri:"name",Header 用 header:"name"

binding:"..." 是校验规则,所有来源通用。

1.3.3 路径参数(URI)

go 复制代码
type GetUserReq struct {
    ID uint64 `uri:"id" binding:"required,numeric"`
}

r.GET("/users/:id", func(c *gin.Context) {
    var req GetUserReq
    if err := c.ShouldBindUri(&req); err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }
    // req.ID 是 uint64,自动转换好了
})

1.3.4 表单(Form / Multipart)

go 复制代码
type GetUserReq struct {
    ID uint64 `uri:"id" binding:"required,numeric"`
}

r.GET("/users/:id", func(c *gin.Context) {
    var req GetUserReq
    if err := c.ShouldBindUri(&req); err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }
    // req.ID 是 uint64,自动转换好了
})

1.3.5 请求头

go 复制代码
type Headers struct {
    Token        string `header:"X-Token"`
    RequestID    string `header:"X-Request-Id" binding:"required,uuid4"`
    AcceptLang   string `header:"Accept-Language"`
}

r.GET("/", func(c *gin.Context) {
    var h Headers
    if err := c.ShouldBindHeader(&h); err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }
    // ...
})

1.3.6 自动选择(ShouldBind)

c.ShouldBind(&req) 会根据 Content-Type 自动选 Binding:

  • GET 请求 → Query
  • POST + application/json → JSON
  • POST + application/x-www-form-urlencoded → Form
  • POST + multipart/form-data → FormMultipart
css 复制代码
if err := c.ShouldBind(&req); err != nil { ... }

1.4 校验规则(validator v10)

Gin 默认使用 go-playground/validator/v10

1.4.1 常用标签速查

标签 含义 示例
required 必填 binding:"required"
min=N / max=N 字符串长度 / 数字范围 / 切片长度 min=3,max=32
len=N 精确长度 len=11(手机号)
oneof=a b c 枚举 oneof=male female
email 邮箱格式
url URL 格式
uuid4 / uuid UUID 格式
numeric / number 数字 / 浮点
alpha / alphanum 字母 / 字母数字
eq=N``ne=N``gt=N``lt=N``gte=N``lte=N 比较
datetime=2006-01-02 Go 时间格式
ip / ipv4 / ipv6 IP 格式
json / base64 / jwt 数据格式
dive 进入切片/Map 元素校验
unique 唯一
excludesall=0 不含某值

1.4.2 综合示例

c 复制代码
type RegisterReq struct {
    Email    string   `json:"email"    binding:"required,email"`
    Username string   `json:"username" binding:"required,alphanum,min=3,max=20"`
    Password string   `json:"password" binding:"required,min=8,max=64"`
    Age      int      `json:"age"      binding:"gte=18,lte=120"`
    Gender   string   `json:"gender"   binding:"required,oneof=male female other"`
    Tags     []string `json:"tags"     binding:"max=5,dive,min=2"`
    Site     string   `json:"site"     binding:"url"`
    Birthday string   `json:"birthday" binding:"datetime=2006-01-02"`
}

1.4.3 跨字段校验

c 复制代码
type ChangePwdReq struct {
    OldPassword string `json:"old_pwd" binding:"required"`
    NewPassword string `json:"new_pwd" binding:"required,nefield=OldPassword,min=8"`
}

nefield=X 表示不能等于字段 X。

1.4.4 嵌套校验

c 复制代码
type OrderReq struct {
    Customer CustomerReq `json:"customer" binding:"required"`
    Items    []ItemReq   `json:"items"    binding:"required,min=1,dive"`
}

type CustomerReq struct {
    Name  string `json:"name"  binding:"required"`
    Email string `json:"email" binding:"required,email"`
}

type ItemReq struct {
    SKU   string `json:"sku"   binding:"required"`
    Count int    `json:"count" binding:"gte=1"`
}

1.5 自定义错误信息

validator 的默认错误信息对用户不友好(如 Key: 'LoginReq.Password' Error:Field validation for 'Password' failed on the 'min' tag)。

方案 1:简单翻译

go 复制代码
func translateError(err error) string {
    errs, ok := err.(validator.ValidationErrors)
    if !ok {
        return err.Error()
    }
    msg := make([]string, 0, len(errs))
    for _, e := range errs {
        switch e.Tag() {
        case "required":
            msg = append(msg, fmt.Sprintf("%s 不能为空", e.Field()))
        case "min":
            msg = append(msg, fmt.Sprintf("%s 长度不能小于 %s", e.Field(), e.Param()))
        case "email":
            msg = append(msg, "邮箱格式不正确")
        // ...
        }
    }
    return strings.Join(msg, "; ")
}

// 使用
if err := c.ShouldBindJSON(&req); err != nil {
    c.JSON(400, gin.H{"err": translateError(err)})
    return
}

方案 2:用 ut 统一翻译(国际化)

参考 go-playground/validator README,

集成 universal-translator,可输出多语言错误信息。


1.6 自定义校验规则

源码位置 :binding/default_validator.go

go 复制代码
import "github.com/go-playground/validator/v10"

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    _ = v.RegisterValidation("mobile", func(fl validator.FieldLevel) bool {
        m := fl.Field().String()
        return regexp.MustCompile(`^1[3-9]\d{9}$`).MatchString(m)
    })
}

// 使用
type SmsReq struct {
    Mobile string `json:"mobile" binding:"required,mobile"`
}

📌 注册时机应在 gin.New() 之前(例如放在 init()main() 顶部)。


1.7 多次绑定(Body 重复读)

c.Request.Bodyio.ReadCloser,只能读一次。但有时你需要在中间件记录请求体,又要在 handler 绑定。

Gin 提供 ShouldBindBodyWith(只对 Body 类 Binding 有效):

go 复制代码
r.Use(func(c *gin.Context) {
    var peek map[string]any
    _ = c.ShouldBindBodyWith(&peek, binding.JSON) // 缓存 body 到 Context
    c.Next()
})

r.POST("/", func(c *gin.Context) {
    var req MyReq
    _ = c.ShouldBindBodyWith(&req, binding.JSON)  // 第二次读,从缓存取
})

缓存的 key 是 binding.BodyBytesKey,内部用 c.Set(BodyBytesKey, bodyBytes)


1.8 自定义 Binding

如果需要支持特殊格式(如自定义二进制协议),实现 Binding 接口即可:

go 复制代码
type myBin struct{}
func (myBin) Name() string { return "mybin" }
func (myBin) Bind(req *http.Request, obj any) error {
    data, err := io.ReadAll(req.Body)
    if err != nil { return err }
    return myDecode(data, obj) // 自定义解码
}

// 使用
var req MyReq
if err := c.MustBindWith(&req, myBin{}); err != nil { ... }

1.9 小结

  • ✅ 理解 Binding 接口与 14 种内置 Binding
  • ✅ 永远用 ShouldBind* 系列,不要用 Bind*
  • ✅ 熟悉 validator 常用标签:required / min / max / oneof / email ...
  • ✅ 学会自定义校验规则
  • ✅ 知道 Body 重复读用 ShouldBindBodyWith
相关推荐
八角丶几秒前
Node 网络编程 —— TLS 模块
javascript·后端·node.js
山岚的运维笔记5 分钟前
mysql 专业笔记 -- 第 8 章:使用变量
运维·数据库·笔记·后端·学习·mysql·dba
平头哥AI1 小时前
Day 01 | go run 跑通第一个 Go 程序,go build 留下一个能拷走的 exe
开发语言·后端·golang
ZGG0033 小时前
线程池详解:从 7 大参数到生产避坑
java·数据库·后端
geovindu4 小时前
CSharp: Wordcloud
开发语言·后端·c#·.net·.netcore·词云
geovindu5 小时前
CSharp: Command Pattern
开发语言·后端·c#·.net·.netcore·命令模式·行为模式
tedcloud1236 小时前
OpenLogi 怎么搭建?用 Rust 打造一个轻量的 Logitech 外设管理工具
linux·运维·服务器·开发语言·后端·rust·开源
学长毕业设计6 小时前
基于SpringBoot的健康食谱管理系统的设计与实现(源码+文档+讲解视频)
java·spring boot·后端
逃逸线LOF7 小时前
Spring的AOP简介
java·后端·spring
yume_sibai7 小时前
02-Rust 所有权与借用深入解析(底层原理 + 借用检查器 + 生命周期 + 内部可变性)
开发语言·后端·rust