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) // ★ 解码后自动校验
}
关键点
json.API是 Gin 在codec/json/包里抽象的 JSON 接口:
-
- 优先使用 sonic(bytedance 高性能 JSON,基于 JIT)
-
- 回退到标准库
encoding/json
- 回退到标准库
validate(obj):解码完自动跑 validator(见 7.6)
EnableDecoderUseNumber:让数字解析为json.Number(可区分 int / float)
scss
// 启用方式(全局)
gin.EnableJsonDecoderUseNumber()
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)
}
流程:
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.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
}
关键设计
sync.Once:validate实例只创建一次,后续复用(validator 实例化开销大)
- 支持 Slice / Array:自动遍历每个元素
- 支持嵌套:递归到指针 / 嵌套 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) error 和 Engine() any),
就能完全替换 validator 实现(如改用其他校验库)。
1.7 Body 缓存:ShouldBindBodyWith
req.Body 是 io.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)
}
机制:
- 第一次调用:读
req.Body,把字节缓存到c.Keys[BodyBytesKey]
- 后续调用:从
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 文件上传:FormFile 与 SaveUploadedFile
源码位置 :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]实现