开发中会频繁的使用各种对象,在Java中称为Javabean,在Go中用结构体。使用ORM框架时,经常会用实体类来映射数据表,但实际上很少会直接使用映射数据表的实体类对象在各层传输,更多的会使用其他对象(如DTO,VO等),对读出的实体类对象的属性进行过滤或增加。
用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…
有疑问加站长微信联系(非本文作者)