Go反射 实现任意类型属性拷贝

开发中会频繁的使用各种对象,在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…