Go反射 实现任意类型属性拷贝
用Java的朋友都知道,有个便利的工具叫BeanUtils,调用一下 copy()
方法即可去除大量的setter操作。Go自带很多package,但并没有任意类型的拷贝方法,内置的 copy()
也只能拷贝切片。
不过Go自带反射包,利用反射,我们可以手动实现一个任意类型属性拷贝的函数或方法。
golang.org/pkg/reflect…
Overview
以下摘自文档
Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf, which returns a Type. A call to ValueOf returns a Value representing the run-time data. Zero takes a Type and returns a Value representing a zero value for that type.
大致意思就是说,通过利用反射,可以在程序运行时处理任意类型。通过 TypeOf
方法取得取得类型信息,包装在 Type
中。通过 ValueOf
取得运行时的数据,包装在 Value
中。
下面介绍 reflect
包中的一些类型及方法。已经熟悉反射包的大佬可以直接跳到最后。
Kind
定义: type Kind uint
用 iota
定义一系列 Kind
常量,表示待处理类型的类型。听起来的很绕,其实很好理解,除了基本类型外,当我们自定义结构体时, Kind
为 Strcut
,当处理的类型为指针时, Kind
为 Ptr
,还有其他的诸如 Slice
, Map
, Arrray
, Chan
等。
在 Value
和 Type
的一些方法只能给特定的类型使用,比如说 Type
的 MapOf()
方法,只能是 Map
使用,当使用的 Type
不是 Map
时会报 panic
。诸如此类的方法还有很多,因为类型不匹配时会直接 panic()
,为了安全,在使用特定方法时应该先对 Kind
进行判断。
Type
定义的 Type
是接口类型,获得实例后可以通过调用一系列方法获得类型相关信息,可以通过 reflect.TypeOf(i interface{})
获得实例。
通过 Name()
方法可以获得类型名称。 Kind()
方法可以获得类型的 Kind
。
如果 Kind
为结构体类型 Struct
,通过 NumField()
可获得结构体的属性个数,可以通过 Field(i int) StructField
, FieldByName(name string) StructField
获得具体的属性,返回值是另外一种定义的结构体类型 StructField
。
如果 Kind
为 Array
, Chan
, Map
, Ptr
, Slice
,可用通过 Elem()
获得具体的元素的 Type
。
一些特定方法只能给特定的类型使用,使用不当会直接 panic()
。
两种 Type
是可比较的,可以使用 == 或 != 。
StructField
用来描述结构体中单个属性,定义如下
type StructField struct { Name string PkgPath string Type Type Tag StructTag Offset uintptr Index []int Anonymous bool } 复制代码
其中 Name
为属性名称, PkgPath
为包路径, Type
为属性的类型信息, Tag
为标签(常用来处理编码解码问题,有兴趣的朋友可以看一下相关库和源码)。
Value
当使用 Type
时,我们只能获取到类型的相关信息,若需要操作具体值,我们就得使用 Value
,通过 reflect.ValueOf(i interface{})
。与 Type
不同, Value
的类型为结构体。 Value
也有跟 Type
相似的方法,如 NumField()
, Field(i)
, FieldByName(name string)
, Elem()
等。
此外 Value
还有一系列set方法,如果值可以设置,那么我们可以动态改变值。
与 Type
不同, Value
的比较是不可以用 == 或 != ,必须通过相应方法来进行比较。
同样的,一些特定方法只能给特定的类型使用,使用不当会直接 panic()
。
实践
上文简单了解了一下反射的基础。相信很多人都知道怎么实现了。
大致思路:因为需要改变值,所以目标参数传递时必须使用结构体指针,而来源参数可以传指针或者实例。遍历需拷贝类型的所有属性值,用 Field(i int)
获取单一属性,取出 StuctField
的 Name
,再用 Name
通过 FieldByName(name string)
获取被拷贝对象的值,如果获取成功,则调用 Set(v Value)
动态设置值。
coding
func SimpleCopyProperties(dst, src interface{}) (err error) { // 防止意外panic defer func() { if e := recover(); e != nil { err = errors.New(fmt.Sprintf("%v", e)) } }() dstType, dstValue := reflect.TypeOf(dst), reflect.ValueOf(dst) srcType, srcValue := reflect.TypeOf(src), reflect.ValueOf(src) // dst必须结构体指针类型 if dstType.Kind() != reflect.Ptr || dstType.Elem().Kind() != reflect.Struct { return errors.New("dst type should be a struct pointer") } // src必须为结构体或者结构体指针 if srcType.Kind() == reflect.Ptr { srcType, srcValue = srcType.Elem(), srcValue.Elem() } if srcType.Kind() != reflect.Struct { return errors.New("src type should be a struct or a struct pointer") } // 取具体内容 dstType, dstValue = dstType.Elem(), dstValue.Elem() // 属性个数 propertyNums := dstType.NumField() for i := 0; i < propertyNums; i++ { // 属性 property := dstType.Field(i) // 待填充属性值 propertyValue := srcValue.FieldByName(property.Name) // 无效,说明src没有这个属性 || 属性同名但类型不同 if !propertyValue.IsValid() || property.Type != propertyValue.Type() { continue } if dstValue.Field(i).CanSet() { dstValue.Field(i).Set(propertyValue) } } return nil } 复制代码
小结
至此,我们已经完成了同名属性拷贝。因为使用 reflect
包时,到处都有 panic
,所以在最前面需要用延迟函数 recover
一下 panic
。参数传递时,第二个参数使用指针还时实例请自行斟酌。需要注意的是,该拷贝方法为浅拷贝,换句话说,如果说对象内嵌套有其他的引用类型如 Slice
, Map
等,用此方法完成拷贝后,源对象中的引用类型属性内容发生了改变,该对象对应的属性中内容也会改变。
golang.org/pkg/reflect…