请求绑定 binding(binding/ 包)

1.1 Binding 接口

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

go 复制代码
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
    Name() string
    Bind(*http.Request, any) error
}

两个方法:

  • Name():返回绑定的名字(如 "json"),主要用于日志
  • Bind(req, obj):从请求中提取数据,填充到 obj(struct 指针)

1.1.1 扩展接口

go 复制代码
// binding/binding.go:38-49
type BindingBody interface {
    Binding
    BindBody([]byte, any) error     // 从已读好的字节绑定(支持重复读)
}

type BindingUri interface {
    Name() string
    BindUri(map[string][]string, any) error
}

💡设计意图:

  • BindingBody 让你能把 body 读完再绑定(支持多次绑定同一份 body)
  • BindingUri 单独抽接口,因为 URI 参数不是从 *http.Request 取,而是从 radix tree 提取的 map[string][]string

1.2 内置 14 种 Binding

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

ini 复制代码
var (
    JSON          BindingBody = jsonBinding{}
    XML           BindingBody = xmlBinding{}
    Form          Binding     = formBinding{}
    Query         Binding     = queryBinding{}
    FormPost      Binding     = formPostBinding{}
    FormMultipart Binding     = formMultipartBinding{}
    ProtoBuf      BindingBody = protobufBinding{}
    MsgPack       BindingBody = msgpackBinding{}
    YAML          BindingBody = yamlBinding{}
    Uri           BindingUri  = uriBinding{}
    Header        Binding     = headerBinding{}
    Plain         BindingBody = plainBinding{}
    TOML          BindingBody = tomlBinding{}
    BSON          BindingBody = bsonBinding{}
)

每个绑定都是空结构体的单例------没有状态,只是方法的载体。

1.2.1 自动选择:binding.Default

源码位置 :binding/binding.go:95-120

kotlin 复制代码
func Default(method, contentType string) Binding {
    if method == http.MethodGet {
        return Form
    }
    switch contentType {
    case MIMEJSON:
        return JSON
    case MIMEXML, MIMEXML2:
        return XML
    case MIMEPROTOBUF:
        return ProtoBuf
    case MIMEMSGPACK, MIMEMSGPACK2:
        return MsgPack
    case MIMEYAML, MIMEYAML2:
        return YAML
    case MIMETOML:
        return TOML
    case MIMEMultipartPOSTForm:
        return FormMultipart
    case MIMEBSON:
        return BSON
    default:
        return Form
    }
}

c.ShouldBind(obj) 就是先调 Default 选 binding,再调它。


1.3 Context 上的 Bind 方法

源码位置 :context.go:830-863(节选)

go 复制代码
// ❌ 不推荐:失败时自动 Abort 400
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    err := c.ShouldBindWith(obj, b)
    if err != nil {
        // 区分是否超长
        var maxBytesErr *http.MaxBytesError
        switch {
        case errors.As(err, &maxBytesErr):
            c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind)
        default:
            c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
        }
        return err
    }
    return nil
}

// ✅ 推荐:只返回 error,不写响应
func (c *Context) ShouldBind(obj any) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    return c.ShouldBindWith(obj, b)
}

func (c *Context) ShouldBindJSON(obj any) error {
    return c.ShouldBindWith(obj, binding.JSON)
}
// ... ShouldBindXML / Query / YAML / TOML / Plain / Header

1.3.1 Bind* vs ShouldBind*

系列 失败时 适用
Bind / BindJSON / ... 自动写 400 并 Abort 不推荐
ShouldBind / ShouldBindJSON / ... 只返回 error 推荐

⚠️新手陷阱 :Bind* 会调用 MustBindWith,后者调 AbortWithError,

返回的 JSON 格式是 Gin 默认的(非自定义),且容易和后续 c.JSON 冲突。

1.3.2 ShouldBindWith 实现

源码位置 :context.go(ShouldBindWith)

go 复制代码
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
    return b.Bind(c.Request, obj)
}

就这一行! 把请求和 struct 指针交给具体 Binding 处理。


1.4 JSON 绑定实现

源码位置 :binding/json.go

go 复制代码
type jsonBinding struct{}

func (jsonBinding) Name() string { return "json" }

func (jsonBinding) Bind(req *http.Request, obj any) error {
    if req == nil || req.Body == nil {
        return errors.New("invalid request")
    }
    return decodeJSON(req.Body, obj)
}

func (jsonBinding) BindBody(body []byte, obj any) error {
    return decodeJSON(bytes.NewReader(body), obj)
}

func decodeJSON(r io.Reader, obj any) error {
    decoder := json.API.NewDecoder(r)
    if EnableDecoderUseNumber {
        decoder.UseNumber()                  // 数字解析为 Number 而非 float64
    }
    if EnableDecoderDisallowUnknownFields {
        decoder.DisallowUnknownFields()      // 拒绝多余字段
    }
    if err := decoder.Decode(obj); err != nil {
        return err
    }
    return validate(obj)                     // ★ 解码后自动校验
}

关键点

  1. json.API 是 Gin 在 codec/json/ 包里抽象的 JSON 接口:
    • 优先使用 sonic(bytedance 高性能 JSON,基于 JIT)
    • 回退到标准库 encoding/json
  1. validate(obj):解码完自动跑 validator(见 7.6)
  1. EnableDecoderUseNumber:让数字解析为 json.Number(可区分 int / float)
scss 复制代码
// 启用方式(全局)
gin.EnableJsonDecoderUseNumber()
  1. EnableDecoderDisallowUnknownFields:拒绝多余字段
scss 复制代码
gin.EnableJsonDecoderDisallowUnknownFields()

1.5 Form 绑定实现

源码位置 :binding/form.go

go 复制代码
type (
    formBinding          struct{}
    formPostBinding      struct{}
    formMultipartBinding struct{}
)

func (formBinding) Bind(req *http.Request, obj any) error {
    if err := req.ParseForm(); err != nil {
        return err
    }
    if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
        return err
    }
    if err := mapForm(obj, req.Form); err != nil {
        return err
    }
    return validate(obj)
}

流程:

  1. req.ParseForm() --- 解析 URL query 和 body(如果是 form-urlencoded)
  1. req.ParseMultipartForm(32MB) --- 解析 multipart(如果是 multipart/form-data)
  1. mapForm(obj, req.Form) --- 用反射把 url.Values(map[string][]string)填到 struct
  1. validate(obj) --- 跑校验

1.5.1 mapForm ------ 反射映射的核心

源码位置 :binding/form_mapping.go:36-63

go 复制代码
func mapForm(ptr any, form map[string][]string) error {
    return mapFormByTag(ptr, form, "form")
}

func mapFormByTag(ptr any, form map[string][]string, tag string) error {
    ptrVal := reflect.ValueOf(ptr)
    var pointed any
    if ptrVal.Kind() == reflect.Ptr {
        ptrVal = ptrVal.Elem()
        pointed = ptrVal.Interface()
    }
    // 如果目标本身是 map[string]xxx,直接调 setFormMap
    if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String {
        if pointed != nil {
            ptr = pointed
        }
        return setFormMap(ptr, form)
    }
    return mappingByPtr(ptr, formSource(form), tag)   // ★ 否则反射走字段
}

formSource(form)map[string][]string 包装成实现了 setter 接口的对象:

go 复制代码
type formSource map[string][]string

func (form formSource) TrySet(value reflect.Value, field reflect.StructField,
    key string, opt setOptions) (isSet bool, err error) {
    return setByForm(value, field, form, key, opt)
}

mappingByPtr 递归遍历 struct 的每个字段,根据 tag(form:"name")从 formSource 取值填充。

1.5.2 字段类型支持

form_mapping.go 支持的字段类型非常丰富:

类型 转换方式
string 直接赋值
int / int8 / ... / uint / ... strconv.ParseInt
float32 / float64 strconv.ParseFloat
bool strconv.ParseBool
time.Time time_format tag 解析
*multipart.FileHeader 从 multipart form 取文件
嵌套 struct 递归映射
切片 / Map 按索引 / key 映射

📌 tag 多样化:form:"name" 控制字段名,time_format:"2006-01-02" 控制时间格式,

time_location:"Asia/Shanghai" 控制时区。


1.6 Validator 集成

源码位置 :binding/default_validator.go

go 复制代码
type defaultValidator struct {
    once     sync.Once
    validate *validator.Validate
}

var _ StructValidator = (*defaultValidator)(nil)

func (v *defaultValidator) ValidateStruct(obj any) error {
    if obj == nil {
        return nil
    }
    value := reflect.ValueOf(obj)
    switch value.Kind() {
    case reflect.Ptr:
        if value.Elem().Kind() != reflect.Struct {
            return v.ValidateStruct(value.Elem().Interface())
        }
        return v.validateStruct(obj)
    case reflect.Struct:
        return v.validateStruct(obj)
    case reflect.Slice, reflect.Array:
        // 对每个元素单独校验
        count := value.Len()
        validateRet := make(SliceValidationError, 0)
        for i := range count {
            if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
                validateRet = append(validateRet, err)
            }
        }
        // ...
    }
    return nil
}

关键设计

  1. sync.Once:validate 实例只创建一次,后续复用(validator 实例化开销大)
  1. 支持 Slice / Array:自动遍历每个元素
  1. 支持嵌套:递归到指针 / 嵌套 struct

1.6.1 自定义校验

go 复制代码
// 注册自定义规则
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    v.RegisterValidation("mobile", func(fl validator.FieldLevel) bool {
        return regexp.MustCompile(`^1[3-9]\d{9}$`).MatchString(fl.Field().String())
    })
}

详见应用层文档第 4 章。

1.6.2 替换 Validator

ini 复制代码
binding.Validator = &myCustomValidator{}

只要实现 StructValidator 接口(ValidateStruct(any) errorEngine() any),

就能完全替换 validator 实现(如改用其他校验库)。


1.7 Body 缓存:ShouldBindBodyWith

req.Bodyio.ReadCloser,只能读一次。如果你想在中间件和 handler 各绑定一次:

源码位置 :context.go(ShouldBindBodyWith)

go 复制代码
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"

func (c *Context) ShouldBindBodyWith(obj any, bb BindingBody) error {
    var bodyBytes []byte
    if bbts, ok := c.Get(BodyBytesKey); ok {
        bodyBytes = bbts.([]byte)
    } else {
        var err error
        bodyBytes, err = io.ReadAll(c.Request.Body)
        if err != nil {
            return err
        }
        c.Set(BodyBytesKey, bodyBytes)   // ★ 缓存到 Context.Keys
    }
    return bb.BindBody(bodyBytes, obj)
}

机制:

  1. 第一次调用:读 req.Body,把字节缓存到 c.Keys[BodyBytesKey]
  1. 后续调用:从 Keys 取出字节,调 BindingBody.BindBody 重新解码

💡这就是为什么 BindingBody 接口要单独存在------支持从已读字节绑定。

使用场景

go 复制代码
r.Use(func(c *gin.Context) {
    var peek map[string]any
    _ = c.ShouldBindBodyWith(&peek, binding.JSON)   // 中间件读一次
    log.Println(peek)
    c.Next()
})

r.POST("/", func(c *gin.Context) {
    var req MyReq
    _ = c.ShouldBindBodyWith(&req, binding.JSON)    // handler 还能再读
    // ...
})

1.8 URI 绑定

源码位置 :binding/uri.go

go 复制代码
type uriBinding struct{}

func (uriBinding) Name() string { return "uri" }

func (uriBinding) BindUri(m map[string][]string, obj any) error {
    if err := mapURI(obj, m); err != nil {
        return err
    }
    return validate(obj)
}

mapURI 就是 mapFormByTag(ptr, m, "uri")------和 form 映射是同一套机制,只是 tag 换成 uri

go 复制代码
type GetUserReq struct {
    ID uint64 `uri:"id" binding:"required"`
}

📌 URI 参数其实早被 radix tree 解析到 c.Params 了,BindUri 是把 Params

转成 map[string][]string 再用反射映射。


1.9 Header 绑定

源码位置 :binding/header.go

c 复制代码
type Headers struct {
    RequestID string `header:"X-Request-Id"`
    Token     string `header:"Authorization" binding:"required"`
}

类似 URI,只是 tag 换成 header,数据源换成 req.Header


1.10 文件上传:FormFileSaveUploadedFile

源码位置 :context.go:707-759

go 复制代码
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
    if c.Request.MultipartForm == nil {
        if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
            return nil, err
        }
    }
    f, fh, err := c.Request.FormFile(name)
    if err != nil {
        return nil, err
    }
    f.Close()
    return fh, err
}

func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()

    var mode os.FileMode = 0o750
    if len(perm) > 0 {
        mode = perm[0]
    }
    dir := filepath.Dir(dst)
    _, statErr := os.Stat(dir)
    if err = os.MkdirAll(dir, mode); err != nil {
        return err
    }
    if errors.Is(statErr, os.ErrNotExist) {
        if err = os.Chmod(dir, mode); err != nil {
            return err
        }
    }
    return os.WriteFile(dst, /* ... */, mode)
}

设计要点

  • MaxMultipartMemory 控制多大以内放内存,超出会写临时文件(默认 32MB)
  • MkdirAll 自动建目录,只对新建目录 chmod (避免对 /tmp 等已有目录操作失败,见 #4622)

1.11 小结

  • Binding 是统一抽象,14 种内置实现(JSON/Form/URI/Header/...)
  • ShouldBind* 返回 error,Bind* 自动 Abort------总是用前者
  • ✅ Form 绑定通过反射 + tag(form:"name")映射,支持嵌套与丰富类型
  • ✅ validator 集成 go-playground/validator/v10,支持自定义规则
  • ✅ Body 缓存通过 BindingBody.BindBody 接口和 c.Keys[BodyBytesKey] 实现
相关推荐
Cache技术分享1 小时前
504. Java 反射 - 创建一个简单的依赖注入框架
前端·后端
Java内核笔记1 小时前
Spring Boot 4 虚拟线程源码剖析:Tomcat 线程池是怎么被换掉的
spring boot·后端
掘金码甲哥1 小时前
WorkBuddy 直连 DeepSeek v4 Flash,一发截图就崩?我们在网关层一招根治
后端
用户239526180101 小时前
别只会画流程图!彻底搞懂 Spring AI Alibaba Graph 的节点、边与 State
后端
掘金者阿豪1 小时前
接口数据传输优化实战:JSON Gzip 与 Protobuf 的深度对比
后端
用户608186527901 小时前
Avalonia UI 样式进阶实战:外置样式 + MVVM 主题切换 + 样式优先级全解析
后端
掘金者阿豪1 小时前
达梦VS金仓:真正做一次Oracle迁移,才知道工具链有多重要
后端
刘立军1 小时前
中心化配置与 I18n:严格禁止 AI 魔法值与硬编码参数
人工智能·后端·架构
Y001112361 小时前
springboot+vue项目实战
vue.js·spring boot·后端