定义
reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime.
——维基百科
从定义中我们可以看出,反射是计算机程序在运行时(注意是运行时,而不是编译时)拥有获取并修改自身结构的能力。
反射并不是某一种编程语言的专利,而是在众多语言中广泛应用,例如Java、Python、PHP以及我们今天要讲的Go语言。具体>到特定到语言,具体实现哪些反射特性,以及如何实现就各有千秋了。
典型的反射一般包括如下功能:
获取和修改源代码的结构(例如类、方法、字段等)
从字符串类型的类描述符构造出特定的类和对象
检验并执行一段文本的源代码
发展历史
远古时代,那时候编程还只能使用针对特殊硬件架构的指令,代码实际上是一堆机器指令,权限非常大,可以操作具体的硬件,也可以将一串指令作为变量,因此,代码实际上就具有了运行时的动态特性。
中古时代,开始出现编译型的语言,例如Pascal,Fortron,以及后来的C语言,由于所有的代码逻辑都是预先编写好,在编译时已经确定了,因此代码又失去了运行时的动态性。
近代,Brian Cantwell Smith于1982年在他的博士论文中提出了运行时反射的概念。
Java等高级编程语言提供了反射特性,在某些场景中提供了极大的灵活性,例如Java中的Spring框架等等。
reflection in Go
具体到Go语言,秉承Go语言一贯的克制与简单,反射的使用也相对容易。当前,Go语言还不支持通过类描述符动态创建对象。
Type and Value
我们在代码中定义的每一个变量,不管是基本的int,float,还是更复杂的struct,interface,都有类型和值两个概念。例如我们定义如下变量:
那么,我们其实是定义了一个类型为int,值为1的整型变量;类型为string,值为"golang"的字符串变量;类型为Car,值为{"Tesla", "red"}的结构体变量;i虽然为接口,但是由于我们将Car类型的对象赋值给了它,因此它的实际类型也是Car,value同为{"Tesla", "red"}。
为了定义类型和值这两个概念,Go中于是定义了、。让我们来简单看一下它们的定义:
可以看出,是一个接口,而是一个struct。显然,Type会有很多实现类,例如int,float32等等;而Value也有很多方法,且很多方法与Type中的方法类似,例如方法,方法等。
登堂入室:获取及修改运行时信息
反射的一大特性就是可以在运行时获取变量的类型和值等信息,这就好比进到了别人家里,“登堂入室”,别人家里摆着什么家具,用了哪些电器,墙壁是什么颜色,地板是什么风格……都一目了然。
知道这些有什么用呢?除了能满足部分人的“偷窥欲”,更多时候,是为了让我们的家更好,例如家里要装修,自然要请设计师到家里来,看看墙壁是不是换一种颜色?窗帘是不是换一种风格?这一切的前提是能够让设计师更好的了解我们房屋的结构信息。
说回到Go,它为我们提供了方便的方法来更好的帮助我们了解我们需要关注的类型信息。我们先来定义一个家庭的房屋基础信息:
接下来,我们就登堂入室,尝试通过反射来获取房屋信息:
执行结果如下:
我们可以看到,在程序输出xHome的类型信息和值信息,并分别输出了Area、RoomNum、WallColor三个属性后,在尝试访问doorKey属性时报错了,提示我们:,意即无法获取私密的属性或方法。由此我们可以看出,虽然我们可以在受到邀请的情况下进入到别人家中,但是对于别人家里的隐私,我们依然无法获取。
这里例子中,我们主要使用了方法和方法,前者返回接口的类型(reflect.Type的实例),后者返回接口的值(reflect.Value的实例)。获取来Type后,对于复杂类型,例如struct,我们可以更进一步的获取其中的字段(StructField类型)、方法(Method类型)等信息。一层层深入,我们将可以看到所有主人开放出来的信息。
不忘初心:接口还原
我们在使用方法获取到一个接口的值信息后,还可以从该值再还原到接口(这里的接口是指interface{},可以接受一切类型)。例如:
结果如下:
可以看出,我们从Value又得到了一个Home对象,但地址不同,说明已经不是同一个对象,而是原来对象的一个拷贝。这一句相当于是一个类型断言,我们断言从Value获取的接口是Home类型的,如果不是,将会报错。
Value不是你想改,想改就能改
有时我们在获取对象的类型和值信息后,还需要修改值,这个时候就要注意了,如果不注意就很容易出错:
运行程序,你将得到如下信息:
这是因为,我们在执行时,由于go是值传递的,因此xHome被当作参数传递到该方法时,会产生一个xHome的拷贝,我们接下来其实一直都是在对xHome的拷贝进行操作,而我们的本意是要修改xHome,而不是它的拷贝,因此go语言为了避免产生这种无意义的修改,就会报错。
那么什么时候我们才可以修改一个对象呢?像通常我们想在一个方法里修改参数值一样,当我们传递指针的时候。
运行结果如下:
注意,我们传递了xHome的地址给方法,如果此时判断,依然会得到false,因为此时xHomeValue是一个指针,我们不是要修改指针,而是要修改指针所指向的对象,因此可以使用方法获取对象,然后修改。
此时,我们总算是顺利的修改了墙壁的颜色,顺利完成了装修!关键点在于使用指针。
典型应用
在以下场景中(限于当前的go语言中),使用反射再合适不过:
字段映射
这是一种非常常见的需求:将一个对象中的字段映射为另一个对象中的对应字段(可能是系统外的,例如数据库表中的字段,等等)。
举一个json转换的例子,其实是将go对象的字段转换为json的字段:
运行结果如下:
利用go自带的包,我们可以读取struct中的字段信息,包括字段类型,tag(即),并转化为json。
类型判断
当一个方法需要判断参数类型,并根据不同类型作出不同处理时,例如我们可以实现一个通用的Add方法用于实现两个数的相加:
运行结果如下:
方法代理
我们可以使用反射来代理调用方法,并在调用前后做一些额外的事情,例如打日志、记录耗时等等。
例如我们可以做一个调用代理,如下:
然后,我们有一个接口,如下:
现在,我们就可以使用proxy包中的Run方法动态的代理我们的方法了:
运行结果如下:
参考资料
领取专属 10元无门槛券
私享最新 技术干货