通过 gin 源码学习如何将结构体初始化

1,561 阅读2分钟

最近在看 gin 的源码,看完了路由部分,不知道该看些什么,所以跑到 gin下的 issues 下看下别人有问题。正好看到了一个 Bind 的实例,就研究了下

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		student := &struct {
			Name []string `form:"name"`
		}{}
		if err := c.Bind(student); err != nil {
			c.JSON(http.StatusInternalServerError, err)
		}else {
			c.JSON(http.StatusOK, student)
		}
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

在命令行中的输入输出结果如下

结构体被赋值的流程

通过输入和输出可以知道Bind的功能是把命令行中的参数正确的赋值给一个匿名的结构体。那么这个过程究竟是如何实现的呢?

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

通过Bind源码可以知道,首先是读取表单,然后使用mapForm函数来利用表单的数据初始化obj。表单的数据是如何处理的这里就暂不追究细节了,但是通过mapForm的签名可以知道req.Form的类型是map[string][]string

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

mapFormByTag的名称就可以表示这个函数的作用了,利用tag来完成formptr的映射,mapForm则就是使用form这个tag来完成映射操作的。这个时候问题就来了,tag是什么?我们可以通过反射来获取结构体的属性的tag,这样就可以通过结构体属性的tag来从form取值并赋值给结构体属性。

package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name []string `form:"name"`
	Age  int      `form:"age"`
}

func main() {
	user := Student{Name: []string{"zhangsan", "lisi"}, Age: 10}
	tValue := reflect.TypeOf(user)

	for i:=0; i<tValue.NumField(); i++ {
		fmt.Println(tValue.Field(i).Tag.Get("form"))
	}
    // Output: name
    // age
}

结构体被赋值的细节

下面就直接进入到处理的函数部分了,总的来说这就是不断递归的操作。

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
	}

	var vKind = value.Kind()

	if vKind == reflect.Ptr {
		var isNew bool
		vPtr := value
		if value.IsNil() {
			isNew = true
			vPtr = reflect.New(value.Type().Elem())
		}
		isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
		if err != nil {
			return false, err
		}
		if isNew && isSetted {
			value.Set(vPtr)
		}
		return isSetted, 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 isSetted 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), tValue.Field(i), setter, tag)
			if err != nil {
				return false, err
			}
			isSetted = isSetted || ok
		}
		return isSetted, nil
	}
	return false, nil
}

查看Kind的基本类型如下,可以知道除了指针和结构体,就属于 go 中的基本数据类型了。在vKind != reflect.Struct && vKind != reflect.Ptr的时候,就可以通过基本类型来对结构体属性进行赋值。

type Kind uint

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

对结构体的基本类型的赋值操作如下,这部分的操作是解析tag

  1. 如果tag是空字符串,那么会给tag一个默认值,是结构体的属性的名称。
  2. 如果tag提供了默认值,那么在setter中没有这个tag的时候,会给结构体属性提供一个默认值。
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
	var tagValue string
	var setOpt setOptions

	tagValue = field.Tag.Get(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, ",")

		if k, v := head(opt, "="); k == "default" {
			setOpt.isDefaultExists = true
			setOpt.defaultValue = v
		}
	}

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

设置默认值的操作可以如下配置,请求的 url 中并没有传递参数 age。对于不设置的tag的用法,大家可以自己尝试。

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		student := &struct {
			Name []string `form:"name"`
			Age int `form:"age,default=10"`
		}{}
		if err := c.Bind(student); err != nil {
			c.JSON(http.StatusInternalServerError, err)
		} else {
			c.JSON(http.StatusOK, student)
		}
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

好了,接下来就要最后一步了,对结构体属性进行赋值:

func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted 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]
		}
		return true, setWithProperType(val, value, field)
	}
}

在结构体属性为指针类型的时候,会稍微复杂一点,写个简单的代码表示下整体的流程。其他部分的代码就是一些校验和细节的考量了。

package main

import (
	"fmt"
	"reflect"
	"testing"
)

type Student struct {
	Name []string `form:"name"`
	Age  *int      `form:"age"`
}

func main() {
	user := &Student{}
	userValue := reflect.ValueOf(user).Elem()
	age := userValue.Field(1)
	fmt.Println("age is nil ", age.Kind(), age.IsNil())
	vPtr := reflect.New(age.Type().Elem())
	vPtr.Elem().SetInt(10)
	age.Set(vPtr)
	fmt.Println("updated user is ", *user.Age)
	// Output: 10
}