2026-04-27 10:10:41 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [后端, Go, Gin, 绑定, 校验]
|
|
|
|
|
|
create time: 2026-04-27 00:00
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 模型绑定和验证
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
Gin 的绑定系统是自动将 HTTP 请求数据解析到 Go 结构体的核心机制。一条 `c.ShouldBindJSON(&req)` 背后经历了内容类型检测、格式解析、类型转换、标签校验等多步流程。掌握它的原理和陷阱,能大幅减少 API 开发中的边界 case。
|
|
|
|
|
|
|
|
|
|
|
|
思考题:Gin 的 `c.ShouldBind()` 能自动区分 JSON 和 form data 吗?它是怎么判断的?
|
|
|
|
|
|
|
|
|
|
|
|
## 正文
|
|
|
|
|
|
|
|
|
|
|
|
### 1. ShouldBind 全家桶
|
|
|
|
|
|
|
|
|
|
|
|
Gin 提供了一套统一的绑定方法,根据数据来源选择对应的方法:
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 数据来源 | 适用场景 |
|
|
|
|
|
|
|------|----------|----------|
|
|
|
|
|
|
| `c.ShouldBindJSON(&v)` | `Content-Type: application/json` | RESTful API body |
|
2026-04-28 08:53:28 +08:00
|
|
|
|
| `c.ShouldBindXML(&v)` | `Content-Type: application/xml` | XML 请求 |
|
2026-04-27 10:10:41 +08:00
|
|
|
|
| `c.ShouldBindQuery(&v)` | URL 查询参数 | `/api/users?page=1` |
|
|
|
|
|
|
| `c.ShouldBind(&v)` | 自动检测 | JSON / form / query 自动选 |
|
|
|
|
|
|
| `c.ShouldBindUri(&v)` | URL 路径参数 | `/users/:id` |
|
|
|
|
|
|
| `c.ShouldBindHeader(&v)` | HTTP 请求头 | Custom headers |
|
|
|
|
|
|
| `c.ShouldBindBodyWith(&v, binding.Form)` | 请求体为 form | 兼容 form 和 JSON |
|
|
|
|
|
|
| `c.ShouldBindFiles(&v)` | Multipart form file | 文件上传 |
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func handler(c *gin.Context) {
|
|
|
|
|
|
// JSON body
|
|
|
|
|
|
var jsonReq CreateUserRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&jsonReq); err != nil { ... }
|
|
|
|
|
|
|
|
|
|
|
|
// Query string: /users?role=admin&page=1
|
|
|
|
|
|
var query PageQuery
|
|
|
|
|
|
if err := c.ShouldBindQuery(&query); err != nil { ... }
|
|
|
|
|
|
|
|
|
|
|
|
// 自动检测:JSON body > form data > query string
|
|
|
|
|
|
var autoReq Request
|
|
|
|
|
|
if err := c.ShouldBind(&autoReq); err != nil { ... }
|
|
|
|
|
|
|
|
|
|
|
|
// URI params: /users/:id
|
|
|
|
|
|
var uri URIParams
|
|
|
|
|
|
if err := c.ShouldBindUri(&uri); err != nil { ... }
|
|
|
|
|
|
|
|
|
|
|
|
// Headers: Authorization: Bearer xxx
|
|
|
|
|
|
var header HeaderParams
|
|
|
|
|
|
if err := c.ShouldBindHeader(&header); err != nil { ... }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> **关键理解:** `c.ShouldBind()` 不是"万能绑定"——它按顺序检测:先试 JSON(根据 Content-Type),再试 form,最后试 query。如果前端发的 Content-Type 不明确,行为可能不符合预期。
|
|
|
|
|
|
|
|
|
|
|
|
**ShouldBind vs MustBind:**
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 校验失败时行为 | 推荐使用 |
|
|
|
|
|
|
|------|---------------|----------|
|
|
|
|
|
|
| `ShouldBind*` | 返回 error,handler 继续 | 推荐,可自定义错误处理 |
|
|
|
|
|
|
| `MustBind*` | 自动 400 响应,Abort | 快速原型 |
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Struct Tag 绑定语法
|
|
|
|
|
|
|
|
|
|
|
|
Gin 使用 struct tag 指定绑定规则:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type CreateUserRequest struct {
|
|
|
|
|
|
// JSON body 绑定,必须字段,长度限制
|
|
|
|
|
|
Name string `json:"name" binding:"required,min=2,max=50"`
|
|
|
|
|
|
Email string `json:"email" binding:"required,email"`
|
|
|
|
|
|
Age int `json:"age" binding:"required,min=1,max=150"`
|
|
|
|
|
|
Role string `json:"role" binding:"oneof=admin user guest"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**常用 binding 标签:**
|
|
|
|
|
|
|
|
|
|
|
|
| 标签 | 作用 | 示例 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| `required` | 必填 | `binding:"required"` |
|
|
|
|
|
|
| `email` | 邮箱格式 | `binding:"email"` |
|
|
|
|
|
|
| `url` | URL 格式 | `binding:"url"` |
|
|
|
|
|
|
| `datetime` | 日期时间格式 | `binding:"datetime=2006-01-02"` |
|
|
|
|
|
|
| `min=N` | 最小值/长度 | `binding:"min=1,max=50"` |
|
|
|
|
|
|
| `max=N` | 最大值/长度 | `binding:"max=100"` |
|
|
|
|
|
|
| `numeric` | 纯数字 | `binding:"numeric"` |
|
|
|
|
|
|
| `alphanum` | 字母数字 | `binding:"alphanum"` |
|
|
|
|
|
|
| `oneof=X Y Z` | 枚举值 | `binding:"oneof=red green blue"` |
|
|
|
|
|
|
| `omitempty` | 可选字段 | 仅 json 标签用 |
|
|
|
|
|
|
|
|
|
|
|
|
> **提问:** `binding:"required"` 对指针类型 `*string` 和空字符串 `""` 分别怎么处理?
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 自动校验的底层流程
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func createUser(c *gin.Context) {
|
|
|
|
|
|
var req CreateUserRequest
|
|
|
|
|
|
|
|
|
|
|
|
// ShouldBindJSON 内部流程:
|
|
|
|
|
|
err := c.ShouldBindJSON(&req)
|
|
|
|
|
|
// 1. 检查 Content-Type 是否为 application/json
|
|
|
|
|
|
// 2. 用 encoding/json 解析 body 到 req
|
|
|
|
|
|
// 3. 获取 validator(全局共享的 structv1)
|
|
|
|
|
|
// 4. 遍历 struct 的 binding 标签,逐项校验
|
|
|
|
|
|
// 5. 返回 *validator.ValidationErrors 或 nil
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"code": 1002, "message": "参数校验失败", "errors": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// ...
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**校验错误的类型:**
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
|
|
|
|
// 类型:*validator.ValidationErrors(内部实现可能变化)
|
|
|
|
|
|
// 包含所有字段的校验失败信息
|
|
|
|
|
|
c.JSON(400, gin.H{
|
|
|
|
|
|
"code": 1002,
|
|
|
|
|
|
"message": "参数校验失败",
|
|
|
|
|
|
"errors": err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 只绑定查询字符串
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type QueryRequest struct {
|
|
|
|
|
|
Page int `form:"page" binding:"required,min=1"`
|
|
|
|
|
|
Limit int `form:"limit" binding:"required,min=1,max=100"`
|
|
|
|
|
|
Role string `form:"role" binding:"omitempty,oneof=admin user"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// URL: /users?role=admin&page=1&limit=20
|
|
|
|
|
|
func listUsers(c *gin.Context) {
|
|
|
|
|
|
var req QueryRequest
|
|
|
|
|
|
if err := c.ShouldBindQuery(&req); err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"message": "query param error"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// req = {Page: 1, Limit: 20, Role: "admin"}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**注意:** `ShouldBindQuery` 只绑定 URL 查询参数(`?key=value`),不绑定 body。
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 数组集合格式(批量操作)
|
|
|
|
|
|
|
|
|
|
|
|
前端传数组参数时,Gin 使用 `[]` 后缀:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type BatchDeleteRequest struct {
|
|
|
|
|
|
IDs []int `form:"id[]" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// URL: /users?ids[]=1&ids[]=2&ids[]=3
|
|
|
|
|
|
// 或: /users?ids[]=1,2,3
|
|
|
|
|
|
func batchDelete(c *gin.Context) {
|
|
|
|
|
|
var req BatchDeleteRequest
|
|
|
|
|
|
if err := c.ShouldBindQuery(&req); err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"message": "ids required"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// req.IDs = []int{1, 2, 3}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**JSON body 中的数组:**
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"user_ids": [1, 2, 3]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type BatchRequest struct {
|
|
|
|
|
|
UserIDs []int `json:"user_ids" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 6. 自定义 Validator
|
|
|
|
|
|
|
|
|
|
|
|
当内置标签不够用时,注册自定义校验器:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 注册全局自定义校验器
|
|
|
|
|
|
func init() {
|
|
|
|
|
|
// 校验用户名:只能包含字母、数字、下划线,3-20 字符
|
|
|
|
|
|
validator.Validator.RegisterValidation(
|
|
|
|
|
|
"username",
|
|
|
|
|
|
func(v validator.FieldLevel) bool {
|
|
|
|
|
|
name := v.Field().String()
|
|
|
|
|
|
if len(name) < 3 || len(name) > 20 {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(name)
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type RegisterRequest struct {
|
|
|
|
|
|
Username string `json:"username" binding:"required,username"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**带错误消息的自定义校验器:**
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 在 handler 中动态注册
|
|
|
|
|
|
func setupValidator() {
|
|
|
|
|
|
v := binding.Validator.Engine().(*validator.Validate)
|
|
|
|
|
|
|
|
|
|
|
|
v.RegisterValidation("phone", func(fl validator.FieldLevel) bool {
|
|
|
|
|
|
return regexp.MustCompile(`^1[3-9]\d{9}$`).MatchString(fl.Field().String())
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
思考题:自定义校验器 `func(v validator.FieldLevel) bool` 中的 `v.Field()` 返回的是什么类型?如果要校验一个 `time.Time` 字段,应该怎么断言?
|
|
|
|
|
|
|
|
|
|
|
|
### 7. 绑定自定义反序列化器
|
|
|
|
|
|
|
|
|
|
|
|
对于需要自定义解析的类型,实现 `bind.Unmarshaller` 接口:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type DateRange struct {
|
|
|
|
|
|
Start time.Time
|
|
|
|
|
|
End time.Time
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 实现 Unmarshal 接口
|
|
|
|
|
|
func (r *DateRange) Unmarshal(param string) error {
|
|
|
|
|
|
parts := strings.Split(param, "-")
|
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
|
|
return fmt.Errorf("invalid date range format")
|
|
|
|
|
|
}
|
|
|
|
|
|
r.Start, _ = time.Parse("2006-01-02", parts[0])
|
|
|
|
|
|
r.End, _ = time.Parse("2006-01-02", parts[1])
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// URL: /events?range=2026-01-01-2026-01-31
|
|
|
|
|
|
func handler(c *gin.Context) {
|
|
|
|
|
|
var req struct {
|
|
|
|
|
|
Range DateRange `form:"range"`
|
|
|
|
|
|
}
|
|
|
|
|
|
c.ShouldBindQuery(&req)
|
|
|
|
|
|
// req.Range.Start = 2026-01-01, Range.End = 2026-01-31
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8. 绑定请求头
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type HeaderParams struct {
|
|
|
|
|
|
ContentType string `header:"Content-Type"`
|
|
|
|
|
|
ContentType string `header:"X-Request-ID"`
|
|
|
|
|
|
Authorization string `header:"Authorization"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func handler(c *gin.Context) {
|
|
|
|
|
|
var h HeaderParams
|
|
|
|
|
|
if err := c.ShouldBindHeader(&h); err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"message": "header error"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 9. 条件绑定与绑定不同结构体
|
|
|
|
|
|
|
|
|
|
|
|
有时需要**同一个接口支持 JSON 和 form 两种格式**,或者**根据条件绑定不同结构体**:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 场景:同一个 endpoint 接受 JSON 或 form 数据
|
|
|
|
|
|
func handler(c *gin.Context) {
|
|
|
|
|
|
var req struct {
|
|
|
|
|
|
Name string `json:"name" form:"name"`
|
|
|
|
|
|
Email string `json:"email" form:"email"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ShouldBind 自动检测 Content-Type 并选择解析方式
|
|
|
|
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"message": "parse error"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 场景:根据条件绑定不同结构体
|
|
|
|
|
|
func handler(c *gin.Context) {
|
|
|
|
|
|
if c.IsAborted() {
|
|
|
|
|
|
c.Next()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
contentType := c.ContentType()
|
|
|
|
|
|
switch contentType {
|
|
|
|
|
|
case "application/json":
|
|
|
|
|
|
var jsonReq JSONRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&jsonReq); err != nil {
|
|
|
|
|
|
c.AbortWithError(400, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理 JSON 请求
|
|
|
|
|
|
case "application/x-www-form-urlencoded":
|
|
|
|
|
|
var formReq FormRequest
|
|
|
|
|
|
if err := c.ShouldBind(&formReq); err != nil {
|
|
|
|
|
|
c.AbortWithError(400, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理 Form 请求
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**ShouldBindBodyWith — 避免重复读取 body:**
|
|
|
|
|
|
|
|
|
|
|
|
Gin 的 JSON 解析器**只能读一次 body**(因为 body 是 io.ReadCloser)。`ShouldBindBodyWith` 会在第一次读取后缓存 body,后续绑定复用缓存:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func handler(c *gin.Context) {
|
|
|
|
|
|
// 先读取验证(可能读取 body)
|
|
|
|
|
|
var auth AuthRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&auth); err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"error": "auth failed"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 再读取不同结构体 — 用 ShouldBindBodyWith 复用 body
|
|
|
|
|
|
var body CreateUserRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil {
|
|
|
|
|
|
c.JSON(400, gin.H{"error": "body parse failed"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 10. 绑定与校验的最佳实践
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type CreateUserRequest struct {
|
|
|
|
|
|
Name string `json:"name" binding:"required,min=2,max=50"`
|
|
|
|
|
|
Email string `json:"email" binding:"required,email"`
|
|
|
|
|
|
Age int `json:"age" binding:"omitempty,min=0,max=150"`
|
|
|
|
|
|
Role string `json:"role" binding:"oneof=admin user guest"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *CreateUserRequest) ValidateCustom() error {
|
|
|
|
|
|
// 需要跨字段校验时,自定义校验逻辑
|
|
|
|
|
|
if strings.Contains(r.Name, "@") {
|
|
|
|
|
|
return errors.New("name cannot contain @")
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func createUser(c *gin.Context) {
|
|
|
|
|
|
var req CreateUserRequest
|
|
|
|
|
|
|
|
|
|
|
|
// 第一步:自动绑定 + 标签校验
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
// 解析结构化错误
|
|
|
|
|
|
if verr, ok := err.(*validator.ValidationErrors); ok {
|
|
|
|
|
|
c.JSON(400, gin.H{
|
|
|
|
|
|
"code": 1002,
|
|
|
|
|
|
"message": "参数校验失败",
|
|
|
|
|
|
"errors": formatValidationErrors(verr),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(400, gin.H{"code": 1002, "message": "解析失败"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二步:自定义校验(跨字段或数据库查询)
|
|
|
|
|
|
if err := req.ValidateCustom(); err != nil {
|
|
|
|
|
|
c.JSON(422, gin.H{"code": 1006, "message": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(201, gin.H{"message": "created"})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// formatValidationErrors 把校验错误格式化为用户友好的消息
|
|
|
|
|
|
func formatValidationErrors(err *validator.ValidationErrors) map[string]string {
|
|
|
|
|
|
errors := make(map[string]string)
|
|
|
|
|
|
for _, e := range err.Errors {
|
|
|
|
|
|
field := e.Field()
|
|
|
|
|
|
tag := e.Tag()
|
|
|
|
|
|
switch tag {
|
|
|
|
|
|
case "required":
|
|
|
|
|
|
errors[field] = "必填字段"
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
errors[field] = "邮箱格式不正确"
|
|
|
|
|
|
case "min":
|
|
|
|
|
|
errors[field] = fmt.Sprintf("最小值为 %s", e.Param())
|
|
|
|
|
|
default:
|
|
|
|
|
|
errors[field] = fmt.Sprintf("%s 不满足 %s 要求", field, tag)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return errors
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> **提问:** `ShouldBindJSON` 遇到未知字段(JSON 中有 struct 没有的 key)时,默认行为是什么?会报错吗?如果不想报错,有什么办法忽略未知字段?
|
2026-04-28 08:53:28 +08:00
|
|
|
|
>
|
|
|
|
|
|
> → 详见 `[[5-binding-validation/unknown-fields]]`
|
2026-04-27 10:10:41 +08:00
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- `[[GIN/gin-architecture]]` — Engine 如何初始化 validator
|
|
|
|
|
|
- `[[GIN/binding-advanced]]` — Map 绑定、默认值、条件绑定
|
|
|
|
|
|
- `[[GIN/error-handling]]` — 校验错误的全局处理中间件
|
|
|
|
|
|
- `[[Go 后端基础]]` — 结构体标签与 JSON 序列化基础
|