全网最详细的 gin框架请求数据绑定Bind 源码解析 -- 帮助你全面了解gin框架的请求数据绑定原理和方法

8 篇文章 0 订阅

 在gin框架中,我们可以将多种请求数据(json, form,uri,header等)直接绑定到我们定义的结构体,底层是通过反射方式获取我们定义在结构体上面的tag来实现请求数据到我们的结构体数据的绑定的。 在gin的底层有2大体系的数据绑定一个是Bind,是个是ShouldBind, 下面我们就从数据绑定入口开始一层层的解开gin数据绑定的神秘面纱!

gin中支持的数据绑定类型

        gin框架中的所有数据的绑定都是通过请求类型的 Content-Type这个 MIME类型来完成的,他所支持的类型如下:

// Content-Type MIME of the most common data formats.
const (
	MIMEJSON              = "application/json"
	MIMEHTML              = "text/html"
	MIMEXML               = "application/xml"
	MIMEXML2              = "text/xml"
	MIMEPlain             = "text/plain"
	MIMEPOSTForm          = "application/x-www-form-urlencoded"
	MIMEMultipartPOSTForm = "multipart/form-data"
	MIMEPROTOBUF          = "application/x-protobuf"
	MIMEMSGPACK           = "application/x-msgpack"
	MIMEMSGPACK2          = "application/msgpack"
	MIMEYAML              = "application/x-yaml"
	MIMEYAML2             = "application/yaml"
	MIMETOML              = "application/toml"
)

我们在Bind和ShouldBind  2大序列 中是使用的XXX 定义

// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
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{}
	TOML          BindingBody = tomlBinding{}
)

上面这些就是gin框架中支持的数据的绑定类型XXX定义, 如 BindJSON,  BindForm,  ShouldBindJSON,    ShouldBinxUri  等。

gin框架中的2大类型的数据绑定方式

        他们实现的功能是一样的,区别在于Bind序列如果数据绑定失败会直接抛异常并退出当前请求,而ShouldBind 则不会中断当前的请求。 原因是 Bind序列使用的是 c.MustBindWith ,注意这里的名字前缀 Must  在go语言的开发中我们通常的做法就是带这个Must的方法,就表示必须要满足的方法, 如果不满足就直接给你个 panic 异常(直接退出当前请求),gin框架也不另外MustXxx 的方法也是必须要满足的,否则panic中断当前请求; 而ShouldBind序列是通过  c.ShouldBindWith 来实现的,他在数据绑定异常时会忽略异常,继续后的的请求。

1.  Bind  序列 示例

他由Bind方法,和 BindXXX 方法主从,他们内部都是调用了c.MustBindWith 方法,这里的XXX 即gin中支持的数据绑定类型,见 gin中支持的数据绑定类型定义 

func (c *Context) Bind(obj any) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.MustBindWith(obj, b)
}

// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj any) error {
	return c.MustBindWith(obj, binding.JSON)
}
// ....

2.  ShouldBind序列 示例

func (c *Context) ShouldBind(obj any) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.ShouldBindWith(obj, b)
}

// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj any) error {
	return c.ShouldBindWith(obj, binding.JSON)
}
// ......

gin 中数据绑定接口定义

        不管是那个序列的数据绑定,他们都是通过实现以下接口来完成具体的数据绑定的,这个也是go语言的一个核心思想 -- 面向接口编程 !  你没有看错就是面向接口编程,而你常见其他语言,如java 等好像都是说的面向对象编程,而go语言的特别就在于此, go语言中把面向接口编程做到了极致!

        gin框架中为数据绑定定义了3个接口来实现不同类型的数据绑定。 


// 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
}

// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
	Binding
	BindBody([]byte, any) error
}

// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it reads the Params.
type BindingUri interface {
	Name() string
	BindUri(map[string][]string, any) error
}

gin中的数据绑定实现

        gin框架中已经给我们实现了多种常见的数据类型的绑定,见 gin中支持的数据绑定类型 。 当然, 如果已有实现中没有你想要的数据类型的绑定或者你想自己动手来实现, 这个也非常简单, 你只要实现上面定义的对应的接口即可! 不知道怎么实现的话你就参考一下gin中已有的实现,哈哈!

gin框架数据绑定实现截图

form数据绑定实现示例

        这里的数据实现比较多, 我们就以 我们最常用的form数据绑定实现为例,和大家一起来学习一下gin中的数据绑定是如何实现的。

1.  数据绑定入口

        下面的formBinding 绑定是普通form的绑定, 另外还有formPostBinding  POST类型的数据绑定, formMultipartBinding 这个是针对媒体上传类型的数据的绑定实现,我们就不一一列举了,他们的实现思路都差不多。


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)
}
2.  请求form数据解析 req.ParseForm() 

// ParseForm populates r.Form and r.PostForm.
//
// For all requests, ParseForm parses the raw query from the URL and updates
// r.Form.
//
// For POST, PUT, and PATCH requests, it also reads the request body, parses it
// as a form and puts the results into both r.PostForm and r.Form. Request body
// parameters take precedence over URL query string values in r.Form.
//
// If the request Body's size has not already been limited by [MaxBytesReader],
// the size is capped at 10MB.
//
// For other HTTP methods, or when the Content-Type is not
// application/x-www-form-urlencoded, the request Body is not read, and
// r.PostForm is initialized to a non-nil, empty value.
//
// [Request.ParseMultipartForm] calls ParseForm automatically.
// ParseForm is idempotent.
func (r *Request) ParseForm() error {
	var err error
	if r.PostForm == nil {
		if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
			r.PostForm, err = parsePostForm(r)
		}
		if r.PostForm == nil {
			r.PostForm = make(url.Values)
		}
	}
	if r.Form == nil {
		if len(r.PostForm) > 0 {
			r.Form = make(url.Values)
			copyValues(r.Form, r.PostForm)
		}
		var newValues url.Values
		if r.URL != nil {
			var e error
			newValues, e = url.ParseQuery(r.URL.RawQuery)
			if err == nil {
				err = e
			}
		}
		if newValues == nil {
			newValues = make(url.Values)
		}
		if r.Form == nil {
			r.Form = newValues
		} else {
			copyValues(r.Form, newValues)
		}
	}
	return err
}
3. 上传类型数据解析 req.ParseMultipartForm

// ParseMultipartForm parses a request body as multipart/form-data.
// The whole request body is parsed and up to a total of maxMemory bytes of
// its file parts are stored in memory, with the remainder stored on
// disk in temporary files.
// ParseMultipartForm calls [Request.ParseForm] if necessary.
// If ParseForm returns an error, ParseMultipartForm returns it but also
// continues parsing the request body.
// After one call to ParseMultipartForm, subsequent calls have no effect.
func (r *Request) ParseMultipartForm(maxMemory int64) error {
	if r.MultipartForm == multipartByReader {
		return errors.New("http: multipart handled by MultipartReader")
	}
	var parseFormErr error
	if r.Form == nil {
		// Let errors in ParseForm fall through, and just
		// return it at the end.
		parseFormErr = r.ParseForm()
	}
	if r.MultipartForm != nil {
		return nil
	}

	mr, err := r.multipartReader(false)
	if err != nil {
		return err
	}

	f, err := mr.ReadForm(maxMemory)
	if err != nil {
		return err
	}

	if r.PostForm == nil {
		r.PostForm = make(url.Values)
	}
	for k, v := range f.Value {
		r.Form[k] = append(r.Form[k], v...)
		// r.PostForm should also be populated. See Issue 9305.
		r.PostForm[k] = append(r.PostForm[k], v...)
	}

	r.MultipartForm = f

	return parseFormErr
}
4. 数据映射 函数 mapForm , mapFormByTag

   注意这里是一个函数,上面2个ParseForm 和 ParseMultipartForm 都是在请求对象上面的方法。

从下面的代码可见, 他这里调用的是mapFormByTag 这个函数,这个即是根据我们定义在结构体中的Tag来映射数据, 这里因为是form类型的数据绑定,所以这个地方的第三个参数就是 form

func mapForm(ptr any, form map[string][]string) error {
	return mapFormByTag(ptr, form, "form")
}

我们接着看看这个mapFormByTag

这里的ptr就是我们要将数据绑定到的我们自定义的结构体对象的指针, form 这个就是上面解析后的请求表单的数据map,   第三个参数 tag 这个就是我们要解析的数据类型的Tag定义名称,这里就的 form 就表示他解析的数据就是我们的结构体TAG中的名称为form的Tag数据, 如我们结构体中的字段Page的定义  Page int `json:"page" form:"page" `  , 这里的tag名称就是form,而对于的字段名称就是 page, 就表示他可以绑定请求参数page的值到 结构体的 Page 字段。


func mapFormByTag(ptr any, form map[string][]string, tag string) error {
	// Check if ptr is a map
	ptrVal := reflect.ValueOf(ptr)
	var pointed any
	if ptrVal.Kind() == reflect.Ptr {
		ptrVal = ptrVal.Elem()
		pointed = ptrVal.Interface()
	}
	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)
}

PS: 这里有一个很容易忽略但又非常重要的知识点,就是Elem这个方法的应用时机。 当我们在对一个对象应用函数 reflect.ValueOf() 获取对于的 reflect.Value 对象后, 如果any类型的入参 ptr是一个指针,则获取到的Value对象就必须要调用 .Elem()方法获取指针对应的具体的数据的 reflect.Value后再进行操作,否则就获取不到你想要的数据,因为你拜佛没有找对庙门,哈哈!

5. mappingByPtr函数

这个就是具体的数据绑定映射函数的实现逻辑了, 他这个里面用了递归方式来处理数据的映射,另外还使用了一个 setter 数据设置接口来进行数据的设置。 

从下面的代码我们可以看到,在mapping的第一行就继续了一个tag名称的判断,如果名称是 - 就直接返回 忽略了这个字段的映射处理。  如我们结构体中某个字段的tag 名称是是这样定义的  Name string `form:"-" `  这个就表示会忽略Name这个字段的form数据的绑定


func mappingByPtr(ptr any, setter setter, tag string) error {
	_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
	return err
}


func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
	if field.Tag.Get(tag) == "-" { // just ignoring this field
		return false, nil
	}

	vKind := value.Kind()

	if vKind == reflect.Ptr {
		var isNew bool
		vPtr := value
		if value.IsNil() {
			isNew = true
			vPtr = reflect.New(value.Type().Elem())
		}
		isSet, err := mapping(vPtr.Elem(), field, setter, tag)
		if err != nil {
			return false, err
		}
		if isNew && isSet {
			value.Set(vPtr)
		}
		return isSet, nil
	}

	if vKind != reflect.Struct || !field.Anonymous {
		ok, err := tryToSetValue(value, field, setter, tag)
		if err != nil {
			return false, err
		}
		if ok {
			return true, nil
		}
	}

	if vKind == reflect.Struct {
		tValue := value.Type()

		var isSet bool
		for i := 0; i < value.NumField(); i++ {
			sf := tValue.Field(i)
			if sf.PkgPath != "" && !sf.Anonymous { // unexported
				continue
			}
			ok, err := mapping(value.Field(i), sf, setter, tag)
			if err != nil {
				return false, err
			}
			isSet = isSet || ok
		}
		return isSet, nil
	}
	return false, nil
}
6 setter数据设置接口定义

这个接口就定义了一个方法, TrySet 尝试帮我们设置数据

// setter tries to set value on a walking by fields of a struct
type setter interface {
	TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
}
7. 数据设置函数 tryToSetValue 

这里就是数据映射过程中的字段Tag值的获取核心函数。 通过下面的代码我们可以找到gin的数据绑定的Tag中的数据是如何处理的。    详见下面的代码注释


func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
	var tagValue string
	var setOpt setOptions

    // 通过反射获取结构体字段tag对应的数据,
	tagValue = field.Tag.Get(tag)
    //将获取到的tag数据再使用逗号分隔 
	tagValue, opts := head(tagValue, ",")

	if tagValue == "" { // default value is FieldName
		tagValue = field.Name
	}
	if tagValue == "" { // when field is "emptyField" variable
		return false, nil
	}

	var opt string
	for len(opts) > 0 {
		opt, opts = head(opts, ",")

        // 如果获取到的tag值中包含了 default=xx  则对这个字段设置默认值
		if k, v := head(opt, "="); k == "default" {
			setOpt.isDefaultExists = true
			setOpt.defaultValue = v
		}
	}

	return setter.TrySet(value, field, tagValue, setOpt)
}

        根据上面的代码 举例说明:  field.Tag.Get(tag)  这个就是获取我们在结构体中设置的tag对应的值, 如 假设tag为form, 我们有一个结构体中的字段定义是  Page int `json:"page" form:"page,default=1" `     这里的代码field.Tag.Get(tag)  获取到的内容就是 page,default=1

tagValue, opts := head(tagValue, ",") 这个获取到的是tagValue就是 page, opts的值就是default=1

这个定义的意思就是 将请求表单中的 page 对应的字段帮我们绑定到我们定义的这个结构体的 Page字段上面,如果请求表单中没有相关的数据则使用这里定义的默认值1(default=1就是定义默认值), 这个地方就是如何给绑定数据设置默认值的方式, 这个知识点gin官方文档和示例可没有哦!! 这个就是通过这里的源码发现的使用方法。

form的setter接口执行 TrySet
// TrySet tries to set a value by request's form source (like map[string][]string)
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {
	return setByForm(value, field, form, tagValue, opt)
}
 form数据设置函数 setByForm

通过下面的代码,可见他可以设置的数据类型有 切片, 数组,还有可序列化的数据(默认),这个可序列化的数据类型就包含所有的可以被序列化的数据。


func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
	vs, ok := form[tagValue]
	if !ok && !opt.isDefaultExists {
		return false, nil
	}

	switch value.Kind() {
	case reflect.Slice:
		if !ok {
			vs = []string{opt.defaultValue}
		}
		return true, setSlice(vs, value, field)
	case reflect.Array:
		if !ok {
			vs = []string{opt.defaultValue}
		}
		if len(vs) != value.Len() {
			return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
		}
		return true, setArray(vs, value, field)
	default:
		var val string
		if !ok {
			val = opt.defaultValue
		}

		if len(vs) > 0 {
			val = vs[0]
		}
		if ok, err := trySetCustom(val, value); ok {
			return ok, err
		}
		return true, setWithProperType(val, value, field)
	}
}
可序列化的数据 设置
// trySetCustom tries to set a custom type value
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
	switch v := value.Addr().Interface().(type) {
	case BindUnmarshaler:
		return true, v.UnmarshalParam(val)
	}
	return false, nil
}

 这个就是可序列化的数据的设置的具体逻辑,这里也是根据反射方式先获取要设置的结构体的字段的类型,然后根据不同的类型来设置具体的值。  细心的你应该能够注意到,我们上面提到的小知识点 指针类型的数据需要先调用 .Elem()方法 的应用,见下面的 case reflect.Ptr:  


func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
	switch value.Kind() {
	case reflect.Int:
		return setIntField(val, 0, value)
	case reflect.Int8:
		return setIntField(val, 8, value)
	case reflect.Int16:
		return setIntField(val, 16, value)
	case reflect.Int32:
		return setIntField(val, 32, value)
	case reflect.Int64:
		switch value.Interface().(type) {
		case time.Duration:
			return setTimeDuration(val, value)
		}
		return setIntField(val, 64, value)
	case reflect.Uint:
		return setUintField(val, 0, value)
	case reflect.Uint8:
		return setUintField(val, 8, value)
	case reflect.Uint16:
		return setUintField(val, 16, value)
	case reflect.Uint32:
		return setUintField(val, 32, value)
	case reflect.Uint64:
		return setUintField(val, 64, value)
	case reflect.Bool:
		return setBoolField(val, value)
	case reflect.Float32:
		return setFloatField(val, 32, value)
	case reflect.Float64:
		return setFloatField(val, 64, value)
	case reflect.String:
		value.SetString(val)
	case reflect.Struct:
		switch value.Interface().(type) {
		case time.Time:
			return setTimeField(val, field, value)
		case multipart.FileHeader:
			return nil
		}
		return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
	case reflect.Map:
		return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
	case reflect.Ptr:
		if !value.Elem().IsValid() {
			value.Set(reflect.New(value.Type().Elem()))
		}
		return setWithProperType(val, value.Elem(), field)
	default:
		return errUnknownType
	}
	return nil
}
时间类型的字段数据绑定设置setTimeField 

从下面的源码我们可知,对应时间类型的数据绑定,我们除了可以指定字段名称等之外,还可以通过  增加标签 time_format标签来指定时间布局格式, 通过 time_utc标签来是定是否是UTC时间, 通过  time_location标签来指定本地时间



func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
	timeFormat := structField.Tag.Get("time_format")
	if timeFormat == "" {
		timeFormat = time.RFC3339
	}

	switch tf := strings.ToLower(timeFormat); tf {
	case "unix", "unixnano":
		tv, err := strconv.ParseInt(val, 10, 64)
		if err != nil {
			return err
		}

		d := time.Duration(1)
		if tf == "unixnano" {
			d = time.Second
		}

		t := time.Unix(tv/int64(d), tv%int64(d))
		value.Set(reflect.ValueOf(t))
		return nil
	}

	if val == "" {
		value.Set(reflect.ValueOf(time.Time{}))
		return nil
	}

	l := time.Local
	if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
		l = time.UTC
	}

	if locTag := structField.Tag.Get("time_location"); locTag != "" {
		loc, err := time.LoadLocation(locTag)
		if err != nil {
			return err
		}
		l = loc
	}

	t, err := time.ParseInLocation(timeFormat, val, l)
	if err != nil {
		return err
	}

	value.Set(reflect.ValueOf(t))
	return nil
}

时间类型的使用方法见另外一篇文章 gin框架中form, uri 2种类型的数据绑定到自定义结构体, 绑定数据默认值设置方法, 时间格式绑定和格式设置 详解-CSDN博客

ok, 至此,gin框架中的数据请求绑定源码都扒完了...   后面就是如何使用了,当你了解了他的原理后使用那就是小菜一碟了, 本文就不做讨论了。。。。。。

如果本文对你有帮助,欢迎点赞,收藏,评论, 你的支持就是我们继续产出优质内容的动力哦 :) 

  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值