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][]string
1.2 内置 14 种 Binding
源码位置:binding/binding.go:75-91
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
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(节选)
// ❌ 不推荐:失败时自动 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*
系列 | 失败时 | 适用 |
| 自动写 400 并 Abort | 不推荐 |
| 只返回 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.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) // ★ 解码后自动校验 }关键点
json.API是 Gin 在codec/json/包里抽象的 JSON 接口:
- 优先使用sonic(bytedance 高性能 JSON,基于 JIT)
- 回退到标准库
encoding/json
- 回退到标准库
validate(obj):解码完自动跑 validator(见 7.6)
EnableDecoderUseNumber:让数字解析为json.Number(可区分 int / float)
// 启用方式(全局) gin.EnableJsonDecoderUseNumber()EnableDecoderDisallowUnknownFields:拒绝多余字段
gin.EnableJsonDecoderDisallowUnknownFields()1.5 Form 绑定实现
源码位置:binding/form.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) }流程:
req.ParseForm()— 解析 URL query 和 body(如果是 form-urlencoded)
req.ParseMultipartForm(32MB)— 解析 multipart(如果是 multipart/form-data)
mapForm(obj, req.Form)— 用反射把url.Values(map[string][]string)填到 struct
validate(obj)— 跑校验
1.5.1mapForm—— 反射映射的核心
源码位置:binding/form_mapping.go:36-63
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接口的对象:
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支持的字段类型非常丰富:
类型 | 转换方式 |
| 直接赋值 |
|
|
|
|
|
|
| 按 |
| 从 multipart form 取文件 |
嵌套 struct | 递归映射 |
切片 / Map | 按索引 / key 映射 |
📌 tag 多样化:form:"name"控制字段名,time_format:"2006-01-02"控制时间格式,time_location:"Asia/Shanghai"控制时区。
1.6 Validator 集成
源码位置:binding/default_validator.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 }关键设计
sync.Once:validate实例只创建一次,后续复用(validator 实例化开销大)
- 支持 Slice / Array:自动遍历每个元素
- 支持嵌套:递归到指针 / 嵌套 struct
1.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 替换 Validator
binding.Validator = &myCustomValidator{}只要实现StructValidator接口(ValidateStruct(any) error和Engine() any),
就能完全替换 validator 实现(如改用其他校验库)。
1.7 Body 缓存:ShouldBindBodyWith
req.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.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。
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
type 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-759
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]实现