vault backup: 2026-04-28 12:34:34
This commit is contained in:
@@ -32,17 +32,38 @@ func handle(c *gin.Context) {
|
||||
|
||||
**自动检测优先级:**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["c.ShouldBind(&obj)"] --> B{读取 Content-Type}
|
||||
B -->|"application/json"| C["ShouldBindJSON"]
|
||||
B -->|"application/xml"| D["ShouldBindXML"]
|
||||
B -->|"application/x-www-form-urlencoded"| E["ShouldBindForm"]
|
||||
B -->|"multipart/form-data"| F["ShouldBindMultipart"]
|
||||
B -->|无或未知类型| G[fallback: 尝试 form binding]
|
||||
C --> H[绑定结果 → err?]
|
||||
D --> H
|
||||
E --> H
|
||||
F --> H
|
||||
G --> H
|
||||
H -->|"有错误"| I["返回 400 + 错误信息"]
|
||||
H -->|"成功"| J["字段填入 obj"]
|
||||
```
|
||||
|
||||
| Content-Type | 绑定方式 |
|
||||
|--------------|----------|
|
||||
| `application/json` | `ShouldBindJSON()` |
|
||||
| `application/xml` | `ShouldBindXML()` |
|
||||
| `form/urlencoded` | `ShouldBindBodyWith(bind.Form, bind.Encoder{})` |
|
||||
| `application/x-www-form-urlencoded` | `ShouldBindForm()` |
|
||||
| `multipart/form-data` | `ShouldBindMultipart()` |
|
||||
| 无或未知 | 尝试 form binding |
|
||||
| 无或未知 | fallback 到 form binding |
|
||||
|
||||
> **提问:** 如果客户端发送了 `application/json` 但没有设置 Content-Type 头,`ShouldBind` 会成功吗?
|
||||
|
||||
答案:会,但可能不如预期——Gin 的 fallback 逻辑会尝试用 form binding 解析,对于 JSON 格式的请求体会报解析错误。生产环境建议要求客户端显式声明 Content-Type。
|
||||
>
|
||||
> <details>
|
||||
> <summary>点击展开答案</summary>
|
||||
>
|
||||
> 不会成功。Gin 的 fallback 逻辑会将未知类型当作 form binding 处理,用 `application/x-www-form-urlencoded` 的方式去解析 JSON 字符串,必然报解析错误。生产环境建议通过中间件强制要求客户端显式声明 `Content-Type`。
|
||||
> </details>
|
||||
|
||||
### 2. Map 作为绑定参数
|
||||
|
||||
@@ -61,7 +82,16 @@ func updateFields(c *gin.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
跳过绑定的字段:使用 ``binding:"-"`` 标签排除:
|
||||
> [!CAUTION] Map 绑定的类型丢失陷阱
|
||||
>
|
||||
> JSON 中的数字 `100` 在 Go 中会变成 `float64`,而不是 `int`。如果后续需要用到具体数值类型,必须做显式转换:
|
||||
> ```go
|
||||
> age, ok := fields["age"].(float64) // JSON 数字 → float64
|
||||
> if !ok { /* 处理类型断言失败 */ }
|
||||
> ```
|
||||
> 所以 **Map 绑定适合写"通用 API"**(如配置更新),但不推荐用于结构化业务数据——用结构体 + 校验标签才是更稳妥的选择。
|
||||
|
||||
**跳过绑定的字段:使用 `binding:"-"` 标签排除某个结构体字段,使其不参与任何请求数据绑定。** 这是防止前端篡改敏感字段(如权限、服务端生成 ID)的关键手段——详见 [[7-binding-advanced/skip-binding]]。
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
@@ -93,7 +123,7 @@ func mixedBind(c *gin.Context) {
|
||||
// 拼接条件
|
||||
offset := (page - 1) * pageSize
|
||||
results := db.Offset(offset).Limit(pageSize).
|
||||
Where("name LIKE ?", "%"+keyword+"%").Find(&users)
|
||||
Where("name LIKE ?", "%"+payload.Keyword+"%").Find(&users)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -115,12 +145,31 @@ func handler(c *gin.Context) {
|
||||
|
||||
> **陷阱:** 如果 query 和 body 都有 `page` 字段,先 bind query 再 bind JSON,body 的值会覆盖 query。这通常不是期望的行为。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["请求: ?page=2&keyword=gin"] --> B["bind query page=2 keyword=gin"]
|
||||
C['Body JSON: page=1, name=test'] --> D["bind JSON page=1 name=test"]
|
||||
B --> E["最终结果 page=1 被覆盖"]
|
||||
D --> E
|
||||
```
|
||||
|
||||
**推荐做法:** 分开两个结构体,或者像方案一那样手动提取。
|
||||
|
||||
### 4. 字段默认值策略
|
||||
|
||||
Go 零值机制可以部分替代默认值,但 HTTP 场景下有时需要区分"未提供"和"提供了零值":
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["字段未提供"] --> B{"是否用指针"}
|
||||
B -->|是| C["*int = nil 判为未提供"]
|
||||
B -->|否| D["int = 0 零值无法区分"]
|
||||
A --> E{"字段提供了零值"}
|
||||
E -->|是| F["指针方式也拿不到信号"]
|
||||
F --> G["两种方案都无法区分"]
|
||||
D --> H["用 map 手动标记"]
|
||||
```
|
||||
|
||||
```go
|
||||
// 方法一:使用 pointer 类型判断是否被设置
|
||||
type CreateReq struct {
|
||||
@@ -129,7 +178,7 @@ type CreateReq struct {
|
||||
Priority *int `json:"priority"` // 默认值为 1
|
||||
}
|
||||
|
||||
func handler(c *gin.Context) {
|
||||
func pointerDefaults(c *gin.Context) {
|
||||
var req CreateReq
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
@@ -151,7 +200,7 @@ type SearchReq struct {
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
|
||||
func handler(c *gin.Context) {
|
||||
func manualDefaults(c *gin.Context) {
|
||||
var req SearchReq
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
@@ -165,16 +214,16 @@ func handler(c *gin.Context) {
|
||||
// keyword 允许为空字符串,不需要设默认值
|
||||
}
|
||||
|
||||
// 方法三:利用 query binding 的 DefaultQuery / DefaultString
|
||||
func handler(c *gin.Context) {
|
||||
page := c.DefaultInt("page", 1) // 查询参数默认为 1
|
||||
status := c.DefaultQuery("status", "all") // 查询参数默认为 "all"
|
||||
// 方法三:利用 query binding 的 DefaultQuery / DefaultInt
|
||||
func queryDefaults(c *gin.Context) {
|
||||
page := c.DefaultInt("page", 1) // query 参数默认为 1
|
||||
status := c.DefaultQuery("status", "all") // query 参数默认为 "all"
|
||||
}
|
||||
```
|
||||
|
||||
思考题:为什么 Gin 没有像某些框架那样提供 `default` 结构体标签(如 Spring Boot 的 `@DefaultValue`)?你觉得这种设计的好处是什么?
|
||||
|
||||
提示:考虑 Go 的零值语义 vs 其他语言的区别。
|
||||
> **提示:** 考虑 Go 的零值语义 vs 其他语言的区别。Go 强调"显式优于隐式",默认值逻辑放在业务层而非框架层,让开发者清楚每个字段的来源。这虽然多了几行代码,但避免了隐藏的控制流——当你在调试时,不需要猜某个值是从哪来的。
|
||||
|
||||
### 5. 按条件绑定不同结构体
|
||||
|
||||
@@ -203,20 +252,34 @@ func flexibleHandler(c *gin.Context) {
|
||||
|
||||
> **核心原理:** 第一次调用 `ShouldBindBodyWith` 时会完整读取并缓存 `request.Body`,后续调用直接复用缓存。所以性能代价是**额外占用内存**存储一份请求体副本——只应在需要解耦的场景使用。
|
||||
|
||||
> [!NOTE] 何时使用 ShouldBindBodyWith?
|
||||
> - ✅ 路由中间件已读取过 Body(如日志记录),需要通过 `c.Request.Body = io.NopCloser(bytes.NewBuffer(buf))` 恢复后再绑定
|
||||
> - ✅ 同一请求需要根据某个字段分发给不同处理逻辑
|
||||
> - ❌ 如果只需要读一次 body,直接用 `ShouldBindJSON` 即可,无需多此一举
|
||||
>
|
||||
> Gin 还提供更细粒度的 API:`ShouldBindBodyWith(obj, binding.Binding)` 允许指定绑定策略(`bind.JSON`、`bind.Form` 等),而不是让 Gin 自动猜测。
|
||||
|
||||
### 6. 常见绑定标签速查
|
||||
|
||||
| 标签 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `binding:"required"` | 必填 | `"name" binding:"required"` |
|
||||
| `binding:"omitempty"` | 可选,有值则验证 | |
|
||||
| `binding:"email"` | 邮箱格式 | |
|
||||
| `binding:"url"` | URL 格式 | |
|
||||
| `binding:"numeric"` | 纯数字 | |
|
||||
| `binding:"gte=0,lte=100"` | 范围限制 | 分数 0-100 |
|
||||
| `binding:"len=11"` | 长度限制 | 手机号 |
|
||||
| `binding:"iscolor"` | 颜色值 | |
|
||||
| `` binding:"-" `` | 跳过绑定 | |
|
||||
| `json:"-"` | 跳过序列化 | |
|
||||
Gin 的校验底层使用 `go-playground/validator`。以下为最常用的标签:
|
||||
|
||||
| 标签 | 说明 | 示例代码 |
|
||||
|------|------|----------|
|
||||
| `binding:"required"` | 字段必填,空值则返回 400 | `"name" binding:"required"` |
|
||||
| `binding:"omitempty"` | 可选,若提供则执行后续验证 | `"email" binding:"omitempty,email"` |
|
||||
| `binding:"email"` | 邮箱格式校验 | |
|
||||
| `binding:"uri"` | URI 格式校验 | |
|
||||
| `binding:"numeric"` | 纯数字(整数或浮点) | `"code" binding:"required,numeric"` |
|
||||
| `binding:"gte=0,lte=100"` | 数值范围限制 | `"score" binding:"gte=0,lte=100"` |
|
||||
| `binding:"len=11"` | 固定长度 | `"phone" binding:"required,len=11"` |
|
||||
| `binding:"-"` | 跳过绑定和验证 | `Admin bool \`binding:"-"\`` |
|
||||
| `json:"-"` | 不参与 JSON 序列化 | |
|
||||
|
||||
**实战技巧:** 多个标签可以拼接,用空格分隔:
|
||||
```go
|
||||
// email 可选,但若提供了就必须是有效邮箱格式
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
|
||||
Reference in New Issue
Block a user