Go源码解析:copier库

注:该文章源码分析参考 copier

目录

源码包

代码阅读

1、tag标签说明

2、静态参数

3、整体设计

4、辅助方法说明

I、获取实际的Type和Value

II、Tag处理

III、获取结构体Field切片

IV、检查结构体复制结果

V、对结构体进行设值

5、Copy主方法说明

I、参数说明

II、不可寻址

III、非Valid

IV、可转换结构体的转换

V、两个类型都是map的处理

VI、只有一个类型是结构体的处理

VII、判断数组设置标识

VIII、根据amount进行循环处理

测试代码


Go源码解析:copier库_第1张图片

源码包

包获取路径:  go get github.com/jinzhu/copier
 

代码阅读

1、tag标签说明

must:被must标识的字段必须被复制赋值,如果没有的话,则依据下一个标签进行对应的处理

nopanic:如果被must标识的字段没有复制赋值,声明nopanic会返回error代替panic报错

- : 忽略字段的复制赋值

 

2、静态参数

这边使用了四个静态参数,如下:

const (
	// Denotes that a destination field must be copied to. If copying fails then a panic will ensue.
	tagMust uint8 = 1 << iota

	// Denotes that the program should not panic when the must flag is on and
	// value is not copied. The program will return an error instead.
	tagNoPanic

	// Ignore a destation field from being copied to.
	tagIgnore

	// Denotes that the value as been copied
	hasCopied
)

作用分别是:

tagMust:结构体中标注了“must”标签的字段,必须被复制值,否则视为error

tagNopanic:和tagMust配套使用,如果设置标签“nopanic”,则如果不满足must的条件,不直接报错,而是返回error代替

tagIgnore:标签为“-”,设置该标签的字段直接忽略复制

hasCopied:这个不是标签标识,而是字段复制的标识,在结构体复制结束之后,设置flag为已经复制.依据这个字段来判断是否复制成功

 

3、整体设计

这边先说明整个代码的设计思路,然后读者可以依据该思路去查看Copy主方法说明,能够更清晰的理解对应的代码.

大致的设计思路如下:

  1. 不可寻址和Invalid的数据直接报错或者返回

  2. 判断两个数据结构是不是map,进行map的处理

  3. 数组与结构体的处理,按照类型进行数据遍历

    1. 循环所有字段,解析tag

    2. 判断是否忽略不复制,不复制则跳过

    3. 根据字段名进行赋值

    4. 根据方法名(同名)进行赋值

    5. 赋值成功之后设置本字段的赋值成功标识

    6. 循环所有标签,判断不满足“must”标签的情况,进行相应处理

 

4、辅助方法说明

I、获取实际的Type和Value

在go中,如果一个参数是 *Struct类型的,也就是指针类型,当用这个方法去调用方法获取结构体属性的时候会报错,比如我这边通过一个指针类型去调用FieldByName方法,就会出现下面的错误:

--- FAIL: TestIndirect (0.00s)
panic: reflect: FieldByName of non-struct type [recovered]
	panic: reflect: FieldByName of non-struct type

在Go当中,如果是指针类型,可以通过【.Elem】方法获取对应的实际值。所以这边需要两个方法:

1、通过type判断是否指针类型,返回具体的结构体类型

2、通过value判断是否指针类型,发挥具体的结构体数据。

就是下面我们的两个方法:

func indirect(reflectValue reflect.Value) reflect.Value {
	for reflectValue.Kind() == reflect.Ptr {
		reflectValue = reflectValue.Elem()
	}
	return reflectValue
}

func indirectType(reflectType reflect.Type) reflect.Type {
	for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
		reflectType = reflectType.Elem()
	}
	return reflectType
}

II、Tag处理

tag的处理代码比较简单,这边分成两个方法,分别是:

1、解析tag字符串

2、解析Field对应的tag,获得每个字段对应的tag条件

解析tag字符串这边,通过【,】分隔进行字符串的处理,flag通过二级制的方式进行处理,这也是我们经常在代码里面用到的方式,具体可以查看一下我的二进制工具的文章:自定义比特工具。

// parseTags Parses struct tags and returns uint8 bit flags.
func parseTags(tag string) (flags uint8) {
	for _, t := range strings.Split(tag, ",") {
		switch t {
		case "-":
			flags = tagIgnore
			return
		case "must":
			flags = flags | tagMust
		case "nopanic":
			flags = flags | tagNoPanic
		}
	}
	return
}

解析tag标签字符串是tag处理的一部分,下面这个方法就是处理Field对应和tag的关系,返回的是一个map

// getBitFlags Parses struct tags for bit flags.
func getBitFlags(toType reflect.Type) map[string]uint8 {
	//read note 存储的结构是  FieldName->tag对应的二进制数据(tag标签转换成程序标识)
	flags := map[string]uint8{}
	//read note 根据结构体的类型获取对应的Field切片
	toTypeFields := deepFields(toType)

	// Get a list dest of tags
	//read note 循环Field切片,获取切片对应的tag数据
	for _, field := range toTypeFields {
		tags := field.Tag.Get("copier") //tag标签是【copier】
		if tags != "" {
			//read note tag标签转换成程序处理标识(这边也是使用二进制的处理方式)
			flags[field.Name] = parseTags(tags)
		}
	}
	return flags
}

III、获取结构体Field切片

获取结构体的所有Field的处理方法,这边需要注意的是【Anonymous】这个字段,这个字段声明对应的Field是不是匿名属性,这边对匿名属性进行特殊处理,需要把匿名属性的所有Field添加进去。

//read note 根据结构体类型,获取结构体对应的Field切片,注意这边的【Anonymous】表示的匿名变量,匿名变量的Field这边需要特殊处理
func deepFields(reflectType reflect.Type) []reflect.StructField {
	var fields []reflect.StructField

	//read note 判断是不是结构体,只能对结构体进行处理
	if reflectType = indirectType(reflectType); reflectType.Kind() == reflect.Struct {
		//read note 循环处理对应的所有Field
		for i := 0; i < reflectType.NumField(); i++ {
			v := reflectType.Field(i)
			//read note 对【嵌入(匿名)字段】结构体 的所有结构体进行添加
			if v.Anonymous {
				fields = append(fields, deepFields(v.Type)...)
			} else {
				fields = append(fields, v)
			}
		}
	}

	return fields
}

IV、检查结构体复制结果

这个方法是通过上面给出的 map[string]uint进行标识的判断,根据tag进行二进制处理,对错误进行对应的处理.

// checkBitFlags Checks flags for error or panic conditions.
func checkBitFlags(flagsList map[string]uint8) (err error) {
	// Check flag conditions were met
	//read note 循环map(FieldName->tag对应的二进制数据)
	for name, flags := range flagsList {
		//read note 如果字段没有被复制
		if flags&hasCopied == 0 {
			switch {
			case flags&tagMust != 0 && flags&tagNoPanic != 0:
				//read note 处理1:返回错误信息
				err = fmt.Errorf("Field %s has must tag but was not copied", name)
				return
			case flags&(tagMust) != 0:
				//read note 处理2:直接报错
				panic(fmt.Sprintf("Field %s has must tag but was not copied", name))
			}
		}
	}
	return
}

V、对结构体进行设值

set方法这边是对结构体进行复制,这边有两个参数,分别的to和from,表示被复制的结构体,和复制的结构体,这边把这两个字段记成 Origin Result 字段

处理步骤分别为:

  1. 处理非零值的情况
    1. 如果Result字段是指针类型(前置处理)
      1. Origin字段是指针类型,并且为nil,直接设置Result字段为nil返回
      2. Result字段是nil,赋值初值(包含结构体和指针的处理)
    2. 类型转换处理
      1. 如果Origin类型可以转换为Result类型,进行结构体赋值
      2. 如果Resultsql.Scanner类型,调用Scan方法
      3. 如果Origin是指针类型,调用set(Result,Origin.Elem())方法,注意这边指针需要调用.Elem方法(递归)
      4. 返回false
  2. 零值的情况直接返回true
func set(to, from reflect.Value) bool {

	//read note IsValid返回是否非零值的结果.所以这边的处理是针对非零值
	if from.IsValid() {

		//read note 前置条件:处理to的类型,如果from为空,则直接设置空值返回

		//	to是指针类型特殊处理
		if to.Kind() == reflect.Ptr {
			// set `to` to nil if from is nil
			//read note 如果from是空,则直接设置to为零值返回
			if from.Kind() == reflect.Ptr && from.IsNil() {
				to.Set(reflect.Zero(to.Type()))
				return true
			} else if to.IsNil() {
				//read note 如果to是nil且不满足上面的from为nil的条件,这个时候要给to设置默认值
				to.Set(reflect.New(to.Type().Elem()))
			}
			//read note 指针的转换处理
			to = to.Elem()
		}

		//read note from和to类型的转换处理,这边当from是ptr类型的时候,会调用set进行递归处理

		//read note 如果类型可以进行转换,则要设置对应的值(具体什么类型可以转换需要看一下源码,这里不多赘述)
		if from.Type().ConvertibleTo(to.Type()) {
			to.Set(from.Convert(to.Type()))
		} else if scanner, ok := to.Addr().Interface().(sql.Scanner); ok {
			//read note sql.Scanner 这个不知道具体是干嘛的.
			err := scanner.Scan(from.Interface())
			if err != nil {
				return false
			}
		} else if from.Kind() == reflect.Ptr {
			//read note from是指针类型,处理成结构体进行赋值(相当于递归再往下走)
			return set(to, from.Elem())
		} else {
			//read note 其他不能转换的直接返回false
			return false
		}
	}

	//read note 零值直接返回true,零值不处理
	return true
}

结构体设值流程图:

Go源码解析:copier库_第2张图片 流程图

5、Copy主方法说明

I、参数说明

isSlice bool     //传入的转换后的数据结构是不是切片,初始值为false
amount  = 1      //切片长度,初始为1,表示只有一个结构体
from    = indirect(reflect.ValueOf(fromValue)) //返回对应的参数的值(被复制)
to      = indirect(reflect.ValueOf(toValue))   //返回对应的参数的值(复制结果)

II、不可寻址

判断复制结果是否可寻址,这边直接调用TypeCanAddr方法,如果不可寻址则返回错误error

Go的不可寻址类型可以参考:Golang_Puzzlers

	//read note 不可寻址的数据类型可以参考:https://github.com/hyper0x/Golang_Puzzlers/blob/master/src/puzzlers/article15/q1/demo35.go
	if !to.CanAddr() {
		return errors.New("copy to value is unaddressable")
	}

III、非Valid

如果是!Valid,直接返回,不进行参数复制。文档的说明:

It returns false if v is the zero Value.
	// Return is from value is invalid
	if !from.IsValid() {
		return
	}

IV、可转换结构体的转换

这边如果被复制的数据结构可以转换成复制结果的数据结构,直接进行设置返回。

//read note 返回参数的具体类型
	fromType := indirectType(from.Type())
	toType := indirectType(to.Type())

	// Just set it if possible to assign
	// And need to do copy anyway if the type is struct
	//read note 判断是否非结构体并且可以直接赋值
	if fromType.Kind() != reflect.Struct && from.Type().AssignableTo(to.Type()) {
		to.Set(from)
		return
	}

 

V、两个类型都是map的处理

map的复制处理,这边分为几个步骤,主要的处理就是对key和value的处理.主要分成下面几个步骤:

1、判断key的类型是否可以转换,如果不能转换直接返回

2、如果Result对应的Map为空,则初始化

3、循环遍历Map的所有key,先复制key,如果复制失败则跳过该字段

4、复制value,如果复制失败则跳过该字段(这边的复制key的复制是不一样的,因为key是不可变的,value需要再调用Copy进行复制)

5、如果上面的复制操作都成功了,则对map进行keyvalue的映射

具体的代码也比较简短:

//read note from和to都是map
	if fromType.Kind() == reflect.Map && toType.Kind() == reflect.Map {
		//read note 判断map的key的结构类型是否可以转换,因为这边已经判断是map,所以直接通过 .key来获取,不担心是否报错
		if !fromType.Key().ConvertibleTo(toType.Key()) {
			return
		}
		//read note 判断要转换的Map是否为空,为空则进行初始化
		if to.IsNil() {
			to.Set(reflect.MakeMapWithSize(toType, from.Len()))
		}
		//read note 遍历Map的所有key
		for _, k := range from.MapKeys() {
			//read note 根据to的key类型创建一个新的key
			toKey := indirect(reflect.New(toType.Key()))
			//read note 设置key的值
			if !set(toKey, k) {
				continue
			}

			//read note 设置value值
			toValue := indirect(reflect.New(toType.Elem()))
			if !set(toValue, from.MapIndex(k)) {
				//read note 对嵌套的结构体进行copy
				err = Copy(toValue.Addr().Interface(), from.MapIndex(k).Interface())
				if err != nil {
					continue
				}
			}
			to.SetMapIndex(toKey, toValue)
		}
	}

 

VI、只有一个类型是结构体的处理

如果被复制的结构或者复制的结构有一个不是struct,则直接返回,不进行处理

 

VII、判断数组设置标识

上面的操作已经把map和非结构体的情况处理掉,下面就是结构体\结构体数组的处理。

这边要先设置对应的标识,isSlice是用来标识Result是不是数组的,所以这边会设置对应的标识,并且获取数组对应的长度

	//read note 切片处理:设置切片的长度
	if to.Kind() == reflect.Slice {
		isSlice = true
		if from.Kind() == reflect.Slice {
			amount = from.Len()
		}
	}

 

VIII、根据amount进行循环处理

这边的循环处理主要分成五个步骤:

1、通过isSlice标识,获取对应的Origin结构体数据,如果是数组则根据index获取,否则直接使用Origin

2、获取字段对应的Fieldtag,递归处理

3、复制处理

4、通过isSlice标识,对复制之后的结果进行处理

5、通过map进行状态的校验

第一个步骤的代码如下,这边的处理就是根据index进行当前处理的结构体数据的获取:

	//read note 循环被复制的切片的长度
	for i := 0; i < amount; i++ {
		var dest, source reflect.Value

		//read note Result的结果是数组
		if isSlice {
			// source
			// read note 如果Origin的类型是数组,需要根据index进行获取结构体
			if from.Kind() == reflect.Slice {
				source = indirect(from.Index(i))
			} else {
				// read note 如果Origin的类型是结构体,直接获取该结构体
				source = indirect(from)
			}
			// dest
			dest = indirect(reflect.New(toType).Elem())
		} else {
			//read note Result的结果不是数组,直接获取结构体
			source = indirect(from)
			dest = indirect(to)
		}

第二个步骤的处理,是根据上面的 getBitFlags 方法来获取的,具体的代码如下:

		// Get tag options
		//read note 获取tag的所有标签
		tagBitFlags := map[string]uint8{}
		if dest.IsValid() {
			//read note 根据结构体type获取所有的Field对应的tag标签
			tagBitFlags = getBitFlags(toType)
		}

第三个步骤的处理,这边也是根据类型获取对应的Field进行循环的处理,但是这边会比较特殊的地方就是copier包支持的两个功能

1、复制的结果结构体,和方法同名的字段需要复制方法的返回值

2、复制的结果结构体,和被复制结构体字段同名的方法,需要进行设置(没有返回值)

看一下 Origin的方法和Result字段同名的处理

                //read note Origin的方法和Result字段同名的处理

				if fromField := source.FieldByName(name); fromField.IsValid() && !shouldIgnore(fromField, ignoreEmpty) {
					// has field

					//read note 根据名称获取Result结构体的字段.
					if toField := dest.FieldByName(name); toField.IsValid() {
						if toField.CanSet() {
							if !set(toField, fromField) {
								if err := Copy(toField.Addr().Interface(), fromField.Interface()); err != nil {
									return err
								}
							} else {
								//read note 赋值完成,设置对应的标识
								if fieldFlags != 0 {
									// Note that a copy was made

									//read note 设置复制标识
									tagBitFlags[name] = fieldFlags | hasCopied
								}
							}
						}
					} else {
						// try to set to method
						var toMethod reflect.Value
						//read note 通过名称找到对应的Method
						if dest.CanAddr() {
							toMethod = dest.Addr().MethodByName(name)
						} else {
							toMethod = dest.MethodByName(name)
						}
						//read note 【被转换对象的方法调用】的校验还比较严格,这边可以看出来只能有一个字段,并且字段类型要对应上
						if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
							//read note 调用声明的方法
							toMethod.Call([]reflect.Value{fromField})
						}
					}
				}

看一下 Result方法 与Origin字段同名的处理

            // Copy from method to field
			//read note 处理目标结构体的方法,目标结构体的方法要和被复制结构体的字段名一致,就是这边控制的
			for _, field := range deepFields(toType) {
				name := field.Name

				//read note 根据Result的字段,获取Origin同名的方法
				var fromMethod reflect.Value
				if source.CanAddr() {
					fromMethod = source.Addr().MethodByName(name)
				} else {
					fromMethod = source.MethodByName(name)
				}

				//read note 如果方法符合规则,没有入参,有一个出参,则进行对应方法的调用处理
				if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 && !shouldIgnore(fromMethod, ignoreEmpty) {
					if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() {
						values := fromMethod.Call([]reflect.Value{})
						if len(values) >= 1 {
							//read note 进行字段的设值
							set(toField, values[0])
						}
					}
				}
			}

具体的处理主要是和上面两个操作息息相关,主要的处理操作就是循环Field进行处理

第三步骤所有的代码如下:

        //read note 如果是非零值
		if source.IsValid() {
			//read note 获取结构体的所有Field的信息(数组)
			fromTypeFields := deepFields(fromType)
			// fmt.Printf("%#v", fromTypeFields)
			// Copy from field to field or method

			//todo 循环所有的Field处理
			for _, field := range fromTypeFields {
				name := field.Name

				// Get bit flags for field
				//read note 根据name获取tag数据
				fieldFlags, _ := tagBitFlags[name]

				// Check if we should ignore copying
				//read note ignore标签对结构体的影响处理
				if (fieldFlags & tagIgnore) != 0 {
					continue
				}

				//read note Origin的方法和Result字段同名的处理

				if fromField := source.FieldByName(name); fromField.IsValid() && !shouldIgnore(fromField, ignoreEmpty) {
					// has field

					//read note 根据名称获取Result结构体的字段.
					if toField := dest.FieldByName(name); toField.IsValid() {
						if toField.CanSet() {
							if !set(toField, fromField) {
								if err := Copy(toField.Addr().Interface(), fromField.Interface()); err != nil {
									return err
								}
							} else {
								//read note 赋值完成,设置对应的标识
								if fieldFlags != 0 {
									// Note that a copy was made

									//read note 设置复制标识
									tagBitFlags[name] = fieldFlags | hasCopied
								}
							}
						}
					} else {
						// try to set to method
						var toMethod reflect.Value
						//read note 通过名称找到对应的Method
						if dest.CanAddr() {
							toMethod = dest.Addr().MethodByName(name)
						} else {
							toMethod = dest.MethodByName(name)
						}
						//read note 【被转换对象的方法调用】的校验还比较严格,这边可以看出来只能有一个字段,并且字段类型要对应上
						if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
							//read note 调用声明的方法
							toMethod.Call([]reflect.Value{fromField})
						}
					}
				}
			}

			//read note Result方法 与Origin字段同名的处理

			// Copy from method to field
			//read note 处理目标结构体的方法,目标结构体的方法要和被复制结构体的字段名一致,就是这边控制的
			for _, field := range deepFields(toType) {
				name := field.Name

				//read note 根据Result的字段,获取Origin同名的方法
				var fromMethod reflect.Value
				if source.CanAddr() {
					fromMethod = source.Addr().MethodByName(name)
				} else {
					fromMethod = source.MethodByName(name)
				}

				//read note 如果方法符合规则,没有入参,有一个出参,则进行对应方法的调用处理
				if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 && !shouldIgnore(fromMethod, ignoreEmpty) {
					if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() {
						values := fromMethod.Call([]reflect.Value{})
						if len(values) >= 1 {
							//read note 进行字段的设值
							set(toField, values[0])
						}
					}
				}
			}
		}

第四个步骤的处理,是对最终的复制结果进行处理

    //read note 转换结果Result是切片的处理:分成两种情况,被复制的是 结构体指针 和 结构体
		if isSlice {
			if dest.Addr().Type().AssignableTo(to.Type().Elem()) {
				to.Set(reflect.Append(to, dest.Addr()))
			} else if dest.Type().AssignableTo(to.Type().Elem()) {
				to.Set(reflect.Append(to, dest))
			}
		}

第五个步骤是校验map,校验最终的复制结果。处理返回error.

    //read note 这边是不是会有一个问题,就是err是不是会被覆盖,前面的字段有错误,最后一个没有错误则会覆盖之前的error
		err = checkBitFlags(tagBitFlags)

测试代码

/*
 *  @Author : huangzj
 *  @Time : 2020/12/29 9:16
 *  @Description:
 */

package copier

import (
	"fmt"
	"github.com/jinzhu/copier"
	"testing"
)

type User struct {
	Name string
	Age  int
	Role string
}

func (u *User) DoubleAge() int {
	return u.Age * 2
}

type Employee struct {
	Name      string
	Age       int
	SuperRole string
	DoubleAge int
}

func (e *Employee) Role(role string) {
	e.SuperRole = "通过role得到superRole" + role
}

func TestCopier(t *testing.T) {
	user := User{Name: "dj", Age: 18}
	users := []User{
		{Name: "dj", Age: 18, Role: "Admin"},
		{Name: "dj2", Age: 18, Role: "Dev"},
	}
	employee := Employee{}
	var employees []Employee

	//通过 【Role】 方法转换 User的Role属性到 Employee 的SuperRole -- 这个方法名好像要和被转换的一致才行
	//通过 User 的 【DoubleAge】 方法转换到 Employee 的同名属性
	_ = copier.Copy(&employee, &user)
	fmt.Println(fmt.Sprintf("%#v\n", employee))

	//结构体转换到数组,相当于append操作,不过是类型不同,复制具体属性
	_ = copier.Copy(&employees, &user)
	fmt.Println(fmt.Sprintf("%#v\n", employees))

	//不同的结构体数组的转换
	_ = copier.Copy(&employees, &users)
	fmt.Println(fmt.Sprintf("%#v\n", employee))

	i := 10
	var j int
	err := copier.Copy(&i, &j)

	fmt.Println(err, j)
}

 

你可能感兴趣的:(Go语言学习,#,Go语言官方包,源码阅读)