golang – reflect反射原理与示例

最近封装功能发现不得不用到reflect反射,所以找了一些资料学习了一下。

下面我以一个具体的例子,带大家了解真实场景中的反射用法,并且说明反射的核心思路。

需求

封装访问mysql的Query方法,它执行SQL查询并将结果填充到参数中返回,下面是方法的定义:

调用的是这样的:

代码输出了一行记录:

Query方法第1个参数必须传入slice的地址,这样Query内部才能将append扩容过的slice设置到result中,从而调用方可以访问到数据。

第1个参数还要求slice中的元素是指针类型,这是因为Query内部会逐行的向slice中append每一行数据,指针数组在内存扩容分配的时候性能更好。

下面我们来实现Query方法。

实现

首先我必须声明,在Query中我使用了gorm库执行了SQL参数绑定和执行,但是仅仅使用gorm是无法实现Query方法的,我们还需要大量理解与使用反射解决剩余问题。

鉴别*[]*User

空interface{}内部记录了实际数据的type和value,在reflect库对应了2种类型可以获取到interface内的type和value,分别叫做reflect.TypeOf和reflect.ValueOf方法。

我要做的第一件事情就是先检查一下result的type是不是一个指针,因为我要求传入的是slice的地址:

我们全部以上述调用代码示例中的参数为例进行代码说明与讲解。

先用TypeOf返回一个Type对象type1,其自身类型是reflect.Type,内部实际保存类型是*[]*User。

Type.kind()方法返回数据类型,reflect.Ptr是其中一种类型定义,*[]*User就是指针,所以这一步合法(红色部分)。

但是指针具体指向的是啥,还需要下面进一步判定。

鉴别[]*User

接下来需要看一下指针指向的是不是1个slice,因为我们mysql查询回来的多行数据需要append到这个slice里面。

在反射中,对一个指针类型解引用只需要调用一下Elem()方法,所以下面的判断代码是这样的:

type1是*[]*User,所以对type1解引用相当于*type1,得到type2实际上保存的类型就是[]*User。

所以只需要type2.Kind()看一下是不是slice即可。

鉴别*User

作为一个严谨的实现,其实数据的每一层都需要反射判定,比如我们接下来其实需要确认一下slice里面的元素类型是不是指针类型:

如果更加严谨,我们还应该继续对type3解引用,看一下其类型是不是reflect.Struct,但是现在我们就做到这个程度即可。

调用gorm完成查询

db.Raw这一行代码是gorm库的方法,传入SQL以及要绑定的参数,它就会帮我们完成查询并返回rows对象。

假设我们没有封装Query方法,那么直接使用gorm是这样使用的:

通过迭代rows,并每次调用ScanRows可以将每一行数据反射到结构体User的对应字段中。

但是现在我们Query方法封装的时候传入的result已经是一个interface,这就需要用反射来实现上述原本很简单的事情。

创建User对象

因为gorm的ScanRows需要传入User结构体进行填充,所以我们需要先通过反射创建一个User类型的对象:

因为之前type3是*User,所以对type3解引用可以得到User类型,通过reflect.New可以new一个User类型的对象,返回一个*User地址放到elem里。

调用ScanRows填充User

elem其实是一个reflect.Value变量,内部值是一个创建好的*User。

可以将其转换回到一个空interface{},因为interface可以装任何value以及其type。

所以调用elem.Interface()方法就将其转成了一个好用的interface{},可以作为ScanRows的传参了,因为ScanRows内部也是基于反射支持结构体填充的,所以它的函数定义也是interface{}:

软件的每一层各司其职,就是这么回事。

将*User append到result中

每一行数据都应该append追加到result中返回,我们还记得result是一个*[]*User吧。

代码如下:

reflect.ValueOf可以取到result这个interface{}的value,也就是*[]*User。

通过Elem()解引用后相当于得到了另外1个value,也就是[]*User,也就是传入的slice自身,我们往里追加数据即可。

在反射情况下,Elem()得到value虽然代表了[]*User的值,但其实它是一个reflect.Value。

因此常规的append方法并不能直接用在reflect.Value身上,另外append也不支持interface{}参数,所以我们即便对reflect.Value调用Interface()方法后也不能传给append。

不过官方早就考虑到了,所以提供了一个reflect.Append方法,它接收reflect.Value类型的传参,当然实际其内部反射的是[]*User那个slice。

elem是刚才new分配到的*User,所以上述代码就是将*User追加到了[]*User中,并且返回了扩容后的新slice,行为和append操作一样。

将新slice覆盖到旧slice

如果我们熟悉append,应该知道append后slice可能扩容而导致地址改变,所以使用append的时候总是应该这样:

在有反射的情况下,其实原理也是一样的。

我们需要做的就是把上面的newSlice保存到*result中:

reflect.ValueOf(result).Elem()相当于解引用得到了[]*User这个slice,也就是*result。

Set方法相当于*result = newSlice。

相关资料

代码地址:https://github.com/owenliang/go-advanced/blob/master/reflect-demo/main.go

官方博客介绍reflect:https://blog.golang.org/laws-of-reflection

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~