请求绑定 binding(binding/ 包)

发布时间:2026/8/25 14:54:02
请求绑定 binding(binding/ 包) 1.1Binding接口源码位置:binding/binding.go:30-35// 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 扩展接口// 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][]string1.2 内置 14 种 Binding源码位置:binding/binding.go:75-91var ( 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-120func 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(节选)// ❌ 不推荐:失败时自动 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 / Header1.3.1Bind*vsShouldBind*系列失败时适用Bind / BindJSON / ...自动写 400 并 Abort不推荐ShouldBind / ShouldBindJSON / ...只返回 error推荐⚠️新手陷阱:Bind*会调用MustBindWith,后者调AbortWithError,返回的 JSON 格式是 Gin 默认的(非自定义),且容易和后续c.JSON冲突。1.3.2ShouldBindWith实现源码位置:context.go(ShouldBindWith)func (c *Context) ShouldBindWith(obj any, b binding.Binding) error { return b.Bind(c.Request, obj) }就这一行!把请求和 struct 指针交给具体 Binding 处理。1.4 JSON 绑定实现源码位置:binding/json.gotype 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) // ★ 解码后自动校验 }关键点json.API是 Gin 在codec/json/包里抽象的 JSON 接口:优先使用sonic(bytedance 高性能 JSON,基于 JIT)回退到标准库encoding/jsonvalidate(obj):解码完自动跑 validator(见 7.6)EnableDecoderUseNumber:让数字解析为json.Number(可区分 int / float)// 启用方式(全局) gin.EnableJsonDecoderUseNumber()EnableDecoderDisallowUnknownFields:拒绝多余字段gin.EnableJsonDecoderDisallowUnknownFields()1.5 Form 绑定实现源码位置:binding/form.gotype ( 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) }流程:req.ParseForm()— 解析 URL query 和 body(如果是 form-urlencoded)req.ParseMultipartForm(32MB)— 解析 multipart(如果是 multipart/form-data)mapForm(obj, req.Form)— 用反射把url.Values(map[string][]string)填到 structvalidate(obj)— 跑校验1.5.1mapForm—— 反射映射的核心源码位置:binding/form_mapping.go:36-63func 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接口的对象: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.ParseIntfloat32 / float64strconv.ParseFloatboolstrconv.ParseBooltime.Time按time_formattag 解析*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.gotype 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 }关键设计sync.Once:validate实例只创建一次,后续复用(validator 实例化开销大)支持 Slice / Array:自动遍历每个元素支持嵌套:递归到指针 / 嵌套 struct1.6.1 自定义校验// 注册自定义规则 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 替换 Validatorbinding.Validator myCustomValidator{}只要实现StructValidator接口(ValidateStruct(any) error和Engine() any),就能完全替换 validator 实现(如改用其他校验库)。1.7 Body 缓存:ShouldBindBodyWithreq.Body是io.ReadCloser,只能读一次。如果你想在中间件和 handler 各绑定一次:源码位置:context.go(ShouldBindBodyWith)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) }机制:第一次调用:读req.Body,把字节缓存到c.Keys[BodyBytesKey]后续调用:从Keys取出字节,调BindingBody.BindBody重新解码这就是为什么BindingBody接口要单独存在——支持从已读字节绑定。使用场景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.gotype 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。type GetUserReq struct { ID uint64 uri:id binding:required } URI 参数其实早被 radix tree 解析到c.Params了,BindUri是把Params转成map[string][]string再用反射映射。1.9 Header 绑定源码位置:binding/header.gotype Headers struct { RequestID string header:X-Request-Id Token string header:Authorization binding:required }类似 URI,只是 tag 换成header,数据源换成req.Header。1.10 文件上传:FormFile与SaveUploadedFile源码位置:context.go:707-759func (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]实现