数据绑定与验证
在开发中,将请求体或响应体关联到结构体上,在传参、使用和验证等场景中都非常方便。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 使用 form、json、xml、uri 等多层标签实现一个结构体适配多种数据格式:
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 结构体标签用逗号分隔选项,默认值中避免使用逗号
multi和csv格式用分号;分隔默认值列表- 无效的
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 已通过所有验证
}