注:该文章源码分析参考 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 get github.com/jinzhu/copier
must:被must标识的字段必须被复制赋值,如果没有的话,则依据下一个标签进行对应的处理
nopanic:如果被must标识的字段没有复制赋值,声明nopanic会返回error代替panic报错
- : 忽略字段的复制赋值
这边使用了四个静态参数,如下:
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为已经复制.依据这个字段来判断是否复制成功
这边先说明整个代码的设计思路,然后读者可以依据该思路去查看Copy主方法说明,能够更清晰的理解对应的代码.
大致的设计思路如下:
不可寻址和Invalid的数据直接报错或者返回
判断两个数据结构是不是map,进行map的处理
数组与结构体的处理,按照类型进行数据遍历
循环所有字段,解析tag
判断是否忽略不复制,不复制则跳过
根据字段名进行赋值
根据方法名(同名)进行赋值
赋值成功之后设置本字段的赋值成功标识
循环所有标签,判断不满足“must”标签的情况,进行相应处理
在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
}
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
}
获取结构体的所有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
}
这个方法是通过上面给出的 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
}
set方法这边是对结构体进行复制,这边有两个参数,分别的to和from,表示被复制的结构体,和复制的结构体,这边把这两个字段记成 Origin 和 Result 字段
处理步骤分别为:
- 处理非零值的情况
- 如果Result字段是指针类型(前置处理)
- Origin字段是指针类型,并且为nil,直接设置Result字段为nil,返回
- Result字段是nil,赋值初值(包含结构体和指针的处理)
- 类型转换处理
- 如果Origin类型可以转换为Result类型,进行结构体赋值
- 如果Result是sql.Scanner类型,调用Scan方法
- 如果Origin是指针类型,调用set(Result,Origin.Elem())方法,注意这边指针需要调用.Elem方法(递归)
- 返回false
- 零值的情况直接返回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
}
结构体设值流程图:
isSlice bool //传入的转换后的数据结构是不是切片,初始值为false amount = 1 //切片长度,初始为1,表示只有一个结构体 from = indirect(reflect.ValueOf(fromValue)) //返回对应的参数的值(被复制) to = indirect(reflect.ValueOf(toValue)) //返回对应的参数的值(复制结果)
判断复制结果是否可寻址,这边直接调用Type的CanAddr方法,如果不可寻址则返回错误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")
}
如果是!Valid,直接返回,不进行参数复制。文档的说明:
It returns false if v is the zero Value.
// Return is from value is invalid
if !from.IsValid() {
return
}
这边如果被复制的数据结构可以转换成复制结果的数据结构,直接进行设置返回。
//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
}
map的复制处理,这边分为几个步骤,主要的处理就是对key和value的处理.主要分成下面几个步骤:
1、判断key的类型是否可以转换,如果不能转换直接返回
2、如果Result对应的Map为空,则初始化
3、循环遍历Map的所有key,先复制key,如果复制失败则跳过该字段
4、复制value,如果复制失败则跳过该字段(这边的复制key的复制是不一样的,因为key是不可变的,value需要再调用Copy进行复制)
5、如果上面的复制操作都成功了,则对map进行key到value的映射
具体的代码也比较简短:
//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)
}
}
如果被复制的结构或者复制的结构有一个不是struct,则直接返回,不进行处理
上面的操作已经把map和非结构体的情况处理掉,下面就是结构体\结构体数组的处理。
这边要先设置对应的标识,isSlice是用来标识Result是不是数组的,所以这边会设置对应的标识,并且获取数组对应的长度
//read note 切片处理:设置切片的长度
if to.Kind() == reflect.Slice {
isSlice = true
if from.Kind() == reflect.Slice {
amount = from.Len()
}
}
这边的循环处理主要分成五个步骤:
1、通过isSlice标识,获取对应的Origin结构体数据,如果是数组则根据index获取,否则直接使用Origin
2、获取字段对应的Field的tag,递归处理
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)
}