这是 Gin 最常用、也最容易被误用的一块。
你可以把请求体绑定理解成:
- Binding:把司机递来的纸质材料扫进系统
- Validation:按交规检查材料是否合规
#1. 最常见的 JSON 绑定
type CreateTicketRequest struct {
Plate string `json:"plate" binding:"required"`
Speed int `json:"speed" binding:"required"`
SpeedLimit int `json:"speed_limit" binding:"required"`
OfficerName string `json:"officer_name" binding:"required"`
}
func createTicket(c *gin.Context) {
var req CreateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "处罚单信息不完整或格式错误",
"detail": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "处罚单已创建",
"data": req,
})
}#2. Bind* 和 ShouldBind* 的区别,一定要吃透
这是 Gin 初学者的高频坑。
Bind / BindJSON / BindQuery ...
= 一旦绑定失败,Gin 直接帮你中断并写 400
ShouldBind / ShouldBindJSON / ShouldBindQuery ...
= 失败时把 error 返回给你,你自己决定怎么回官方长期建议你真正写业务时优先理解并掌握 ShouldBind* 这套,因为它给你更完整的控制权。
#3. 为什么很多项目更偏爱 ShouldBind*
因为实际项目里,错误响应往往不是“裸 400 文本”这么简单,而是:
- 统一 JSON 结构
- 携带错误码
- 携带 trace id
- 对不同字段错误做定制提示
这时你就不想让框架先替你把头写死。
#4. Query、Path 也能绑定
Gin 不只会绑 JSON。
type TicketURI struct {
ID string `uri:"id" binding:"required"`
}
func getTicket(c *gin.Context) {
var uri TicketURI
if err := c.ShouldBindUri(&uri); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ticket_id": uri.ID})
}#5. 一个非常实用的误区提醒
请求体默认是流。
也就是说,正常 ShouldBind 这类方法会消费 c.Request.Body。
如果你先绑一次,再绑第二次,第二次大概率就 EOF 了。
第一次扫描证件
└── 原件已经被系统读走
第二次还想拿原件重扫
└── 发现已经没有了如果你真要对同一个 body 尝试多次绑定,要用专门的 ShouldBindBodyWith 方案,而且它会带来额外开销。
所以一般原则是:
一个请求体,尽量只绑定一次。