手写 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.Body 是 io.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