当前位置: 首页 > news >正文

请求绑定与校验

手写c.Query("name")一两个参数还好,几十个表单字段写起来很痛苦。
Gin 的绑定(Bind)机制,能把查询串、表单、JSON Body 自动映射到 struct,并触发校验。


1.1 绑定的本质

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

Binding接口:

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*vsBind*

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

API

失败行为

推荐

c.ShouldBind/ShouldBindJSON/ ...

仅返回 error,需自己处理

c.Bind/BindJSON/ ...

自动写 400 响应,不灵活

⚠️新手陷阱:c.Bind失败时自动c.AbortWithStatus(400),
但响应格式是 Gin 默认的{"error": ...},不符合你团队约定的 JSON 结构。

// ❌ 不推荐 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

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}) })

请求:

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

1.3.2 查询参数

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)

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)

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 请求头

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
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=Nne=Ngt=Nlt=Ngte=Nlte=N

比较

datetime=2006-01-02

Go 时间格式

ip/ipv4/ipv6

IP 格式

json/base64/jwt

数据格式

dive

进入切片/Map 元素校验

unique

唯一

excludesall=0

不含某值

1.4.2 综合示例

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 跨字段校验

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 嵌套校验

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:简单翻译

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

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 有效):

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接口即可:

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
http://www.cnnetsun.cn/news/4070416.html

相关文章:

  • 被苹果放弃的老电脑,我用一个免费工具让它重获新生
  • sva日常学习0
  • RuoYi-Vue Pro 完整上手指南:1 小时搭建带权限与审批的企业级后台
  • AI水印技术解析:从SynthID到C2PA标准,开发者如何管理水印可见性
  • 5分钟跑起跨平台QSP播放器:JavaQuestPlayer完整上手与实测体验
  • PCSX2免费开源模拟器完整指南:如何在家用电脑上高清重玩PS2经典游戏
  • 166、Zephyr RTOS调试与测试基础:调试工具链
  • 虚拟机中OpenFOAM-v2012与Paraview完整安装配置指南
  • Cursor免费使用受阻?一份解决设备限制与机器ID重置的完整指南
  • IoT OTA差分升级:bspatch减小固件体积
  • 如何让《暗黑破坏神2》在现代电脑上满帧运行?D2DX宽屏补丁完整上手指南
  • 每天省下一小时重复劳动的炉石传说插件:HsMod到底能帮你做什么
  • 从一脸懵到轻松绕过:Awesome-WAF 帮你 3 步吃透 Web 应用防火墙攻防测试
  • Ryujinx模拟器终极调优攻略:从装不上到丝滑畅玩的完整实操
  • 系统架构设计师考试模拟题库(下):案例分析与论文指导
  • League Akari 英雄联盟客户端工具箱终极指南:自动选人、流程自动化与对局洞察一网打尽
  • Qwen3.8-27B多模态大模型实战:从技术验货到工程化部署全指南
  • Outfit字体实操指南:免费可商用的几何无衬线字体如何一站式打通品牌视觉统一?
  • 汽车测试谍照背后的产品生命周期管理逻辑与市场策略
  • 构建游戏指令智能系统:从数据建模到安全执行的工程实践
  • YOLO 涨点改进|全网独家复现单通道红外小目标增强 光伏板鸟粪微弱热斑识别、分布式光伏无人机巡检全场景有效涨点
  • 5分钟上手DeepL翻译插件:用开源Chrome划词翻译扩展告别来回切换
  • AI算力市场格局:GPU与FPGA的二元竞争与英特尔AI处理器的破局挑战
  • 溶剂桥接电解液:>4C快充硅负极与−55°C极端低温瓶颈如何打破?
  • 处方外流遇上数据入表:互联网医疗的变现逻辑如何被重写
  • Ryzen CPU降压实战指南:用SMUDebugTool在30分钟内完成首次安全调优
  • Conda虚拟环境全攻略:从依赖隔离到团队协作的Python环境管理
  • 老旧Mac免费升级macOS避坑实战手册:OpenCore Legacy Patcher 5个关键疑问一次讲透
  • RPG Maker MV 加密资源快速解密指南:免费网页工具三步还原立绘与音乐
  • 猫抓浏览器资源嗅探扩展完整指南:3步下载网页视频,M3U8流媒体合并不求人