Skip to content
Go
Gin
数据绑定与验证

数据绑定与验证

在开发中,将请求体或响应体关联到结构体上,在传参、使用和验证等场景中都非常方便。Gin 支持绑定 JSON、XML、YAML 和标准表单值(foo=bar&boo=baz),并使用 go-playground/validator/v10 进行验证。


绑定方法对比

Gin 提供两组绑定方法:

类型 方法 错误处理
Must bind Bind, BindJSON, BindXML, BindQuery, BindYAML 内部调用 AbortWithError(400) 自动中止请求
Should bind ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML 返回 error,由开发者自行处理
// Must bind — 出错自动返回 400
var json Login
if err := c.BindJSON(&json); err != nil { /* 不会执行到这里 */ }

// Should bind — 开发者自主控制响应
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
    c.JSON(422, gin.H{"error": err.Error()}) // 自定义状态码和格式
    return
}

💡 最佳实践:推荐使用 ShouldBind* 系列。它能让你自定义错误状态码(如 422)和错误响应格式,而不是强制使用 Gin 的 400 纯文本错误。


结构体标签

Gin 使用 formjsonxmluri 等多层标签实现一个结构体适配多种数据格式:

type Login struct {
    User     string `form:"user" json:"user" xml:"user" binding:"required"`
    Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
标签 数据来源
json:"..." JSON 请求体
xml:"..." XML 请求体
form:"..." 表单数据或查询字符串
uri:"..." 路径参数
header:"..." 请求头
binding:"..." 验证规则(必填、格式等)
time_format:"..." 时间解析格式

多格式绑定

JSON 绑定

r.POST("/loginJSON", func(c *gin.Context) {
    var json Login
    if err := c.ShouldBindJSON(&json); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // json.User, json.Password 已填充
})
// 请求: {"user": "willow", "password": "123"}

XML 绑定

r.POST("/loginXML", func(c *gin.Context) {
    var xml Login
    if err := c.ShouldBindXML(&xml); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
})
// 请求: <?xml version="1.0" encoding="UTF-8"?><root><user>willow</user><password>123</password></root>

自动推断(根据 Content-Type)

r.POST("/login", func(c *gin.Context) {
    var form Login
    // ShouldBind 根据 Content-Type 自动选择绑定器
    if err := c.ShouldBind(&form); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
})

Query 绑定

r.GET("/search", func(c *gin.Context) {
    var query struct {
        Page int    `form:"page"`
        Q    string `form:"q" binding:"required"`
    }
    if err := c.ShouldBindQuery(&query); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
})

URI 绑定(路径参数)

r.GET("/user/:id", func(c *gin.Context) {
    var uri struct {
        ID int `uri:"id" binding:"required,min=1"`
    }
    if err := c.ShouldBindUri(&uri); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
})

内置验证规则

规则 说明 示例
required 必填,零值视为缺失 binding:"required"
- 跳过验证 binding:"-"
min=, max= 数值/字符串/切片长度限制 binding:"min=3,max=30"
len= 精确长度 binding:"len=11"
eq=, ne= 等于/不等于 binding:"ne=0"
gt=, gte=, lt=, lte= 数值比较 binding:"gte=1,lte=130"
oneof= 枚举值 binding:"oneof=male female"
email 邮箱格式 binding:"email"
url URL 格式 binding:"url"
alpha 仅字母 binding:"alpha"
alphanum 仅字母和数字 binding:"alphanum"
numeric 数字字符串 binding:"numeric"
datetime= 日期时间格式 binding:"datetime=2006-01-02"
excludes= 不包含指定字符 binding:"excludes=!@#"
startswith= / endswith= 前缀/后缀 binding:"startswith=+"

🔬 深入原理:底层使用 go-playground/validator/v10,支持结构体级别验证、自定义翻译错误消息、字段级联验证等高级特性。详细文档见 validator 官方


自定义验证器

注册自定义验证函数来扩展内置规则:

package main

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

// Booking 使用自定义验证规则 bookabledate
type Booking struct {
    CheckIn  time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
    CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
}

// 自定义验证函数:日期不能是过去
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
    date, ok := fl.Field().Interface().(time.Time)
    if !ok {
        return false
    }
    return !time.Now().After(date) // 今天及以后才算有效
}

func main() {
    route := gin.Default()

    // 注册自定义验证器
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("bookabledate", bookableDate)
    }

    route.GET("/bookable", func(c *gin.Context) {
        var b Booking
        if err := c.ShouldBindWith(&b, binding.Query); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        c.JSON(200, gin.H{"message": "Dates are valid!"})
    })

    route.Run(":8085")
}

绑定字段默认值

form 标签支持 default 选项设置默认值(Gin v1.11+):

type Person struct {
    Name      string    `form:"name,default=William"`
    Age       int       `form:"age,default=10"`
    Friends   []string  `form:"friends,default=Will;Bill"`                     // multi/csv 用 `;` 分隔
    Addresses [2]string `form:"addresses,default=foo bar" collection_format:"ssv"`
    LapTimes  []int     `form:"lap_times,default=1;2;3" collection_format:"csv"`
}

r.POST("/person", func(c *gin.Context) {
    var req Person
    if err := c.ShouldBind(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, req)
})

不传任何参数时返回:

{
    "Name": "William",
    "Age": 10,
    "Friends": ["Will", "Bill"],
    "Addresses": ["foo", "bar"],
    "LapTimes": [1, 2, 3]
}

🚨 陷阱

  • Go 结构体标签用逗号分隔选项,默认值中避免使用逗号
  • multicsv 格式用分号 ; 分隔默认值列表
  • 无效的 collection_format 值会导致绑定错误

c.ShouldBind vs c.ShouldBindWith

方法 行为
c.ShouldBind(&obj) 根据 Content-Type 自动选择绑定器
c.ShouldBindWith(&obj, binding.JSON) 显式指定绑定类型
c.ShouldBindJSON(&obj) JSON 绑定的快捷方式
// contentType binding:
binding.JSON   // application/json
binding.XML    // application/xml
binding.Form   // application/x-www-form-urlencoded
binding.Query  // 查询字符串
binding.YAML   // application/yaml
binding.Header // 请求头
binding.ProtoBuf // application/protobuf

💡 大多数情况下用 ShouldBind 即可——它自动检测 Content-Type。如果需要强制特定格式(如忽略 Content-Type),使用 ShouldBindWith


完整的验证示例

type CreateUser struct {
    Username string `json:"username" binding:"required,alphanum,min=3,max=30"`
    Email    string `json:"email"    binding:"required,email"`
    Age      int    `json:"age"      binding:"required,gte=1,lte=130"`
    Role     string `json:"role"     binding:"required,oneof=admin user guest"`
    Tags     []string `json:"tags"   binding:"min=1,max=10"`
}

func createUserHandler(c *gin.Context) {
    var req CreateUser
    if err := c.ShouldBindJSON(&req); err != nil {
        // 提取验证错误信息
        c.JSON(422, gin.H{
            "error":   "validation failed",
            "details": err.Error(),
        })
        return
    }
    // req 已通过所有验证
}