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

愚辛 · · 2921 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

开发中会频繁的使用各种对象,在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常量,表示待处理类型的类型。听起来的很绕,其实很好理解,除了基本类型外,当我们自定义结构体时,KindStrcut,当处理的类型为指针时,KindPtr,还有其他的诸如Slice,Map,Arrray,Chan等。

ValueType的一些方法只能给特定的类型使用,比如说TypeMapOf()方法,只能是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

如果KindArray, 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)获取单一属性,取出StuctFieldName,再用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…


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:愚辛

查看原文:Go反射 实现任意类型属性拷贝

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2921 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传