假如把语言的运行时环境看做集市
假如把语言的运行时环境看做集市,在Ruby语言里,你能够在这个集市里认清楚所有的角色。这是Ruby作为动态语言的一个特性,在运行时环境,Ruby保存了对象的所有元数据,所以开发人员能清楚地列出对象的属性、方法等。
那么,Golang作为一种类C语言的静态语言,通过什么样的机制来识别运行时集市中的角色的呢?
角色信息必然保存在某个地方
如果想识别运行时集市中的角色,其角色信息必然保存在某个地方。有了这个信念,问题就变成了:如何在运行时的集市中拿到角色信息。
代码运行环境
- MacOS:10.13.4
- Golang: version go1.10.1 darwin/amd64
- IDE:VSCode 1.24.0
代码原型
仿照golang 反射中的做法,我们先给出一个代码约定,后面的代码都是基于下面的代码运行得出的结果:
packagemainimport("fmt""reflect")typeboystruct{Namestringageint}typehumaninterface{SayName()SayAge()}func(b*boy)SayName(){fmt.Println(b.Name)}func(b*boy)SayAge(){fmt.Println(b.age)}funcmain(){// 定义接口变量varihuman// 初始化对象,jingwei持有对象指针。jingwei:=&boy{Name:"Jingwei",age:28,}// 因为boy实现了human的两个方法,因此可以把jingwei指给接口变量i=jingwei// 通过反射获取接口i 的类型和所持有的值。t:=reflect.TypeOf(i)v:=reflect.ValueOf(i)fmt.Println(t)fmt.Println(t.Kind())fmt.Println(v)//后续操作//...}代码原型中t和v的打印结果
*main.boy
ptr
&{Jingwei 28}
从上面的打印结果来看,t被识别为 *main.boy 的类型,v则打印出了内容值,总结来看就是:
- t 表示i接口的当前类型,它指向main包下struct boy的指针类型;
- v 表示i接口目前的所存储值,它指向main包下struct boy的指针。
如果进一步追究可以发现,reflect.TypeOf(i)所返回的依然是一个接口(interface),且这个接口所对应的底层数据是rtype,它的数据结构如下(即 rtype 实现了reflect.Type接口):
// rtype is the common implementation of most values.// It is embedded in other, public struct types, but always// with a unique tag like `reflect:"array"` or `reflect:"ptr"`// so that code cannot convert from, say, *arrayType to *ptrType.//// rtype must be kept in sync with ../runtime/type.go:/^type._type.typertypestruct{sizeuintptrptrdatauintptr// number of bytes in the type that can contain pointershashuint32// hash of type; avoids computation in hash tablestflagtflag// extra type information flagsalignuint8// alignment of variable with this typefieldAlignuint8// alignment of struct field with this typekinduint8// enumeration for Calg*typeAlg// algorithm tablegcdata*byte// garbage collection datastrnameOff// string formptrToThistypeOff// type for pointer to this type, may be zero}因此在t上面的调用的所有的 reflect.Type 的方法,其接收方都是一个 ** rtype** 的实例。
获取t指针所指向的对象
上面的t归根到底是一个指针,如果我们想获取t所指向的对象的属性,需要再调用一个函数reflect.Elem()把指针所指向的对象解析出来。
// 获取i所指向的对象的类型e:=t.Elem()fmt.Println(e)fmt.Println(e.Kind())fmt.Println(e.Name())相应的输出如下,这个时候e的底层结构(rtype)所代表的是真正的boy结构了,所调用的方法(reflect.Name(),reflect.Name())返回值也都是这个结构的属性了。
main.boy
struct
boy
获取t指针所指向对象的方法
在golang中,Method也有自己的数据结构,如下
// Method represents a single method.typeMethodstruct{// Name is the method name.// PkgPath is the package path that qualifies a lower case (unexported)// method name. It is empty for upper case (exported) method names.// The combination of PkgPath and Name uniquely identifies a method// in a method set.// See https://golang.org/ref/spec#Uniqueness_of_identifiersNamestringPkgPathstringTypeType// method typeFuncValue// func with receiver as first argumentIndexint// index for Type.Method}如果要获取指针t中的方法,可以通过reflect.Type接口中的 Method(int) Method 获取。
fmt.Println("method")fmt.Println(t.Method(t.NumMethod()-1))fmt.Println(e.NumMethod())上面的代码输出为:
method
{SayName func(*main.boy) <func(*main.boy) Value> 1}
0
如果要获取接口中所有暴露的方法,可以通过便利的方式(首先通过NumMethod()获取方法数量,然后遍历即可)很容易就可以做到。
fmt.Println(e.NumMethod())输入为0,说明到了boy这一层,方法列表的信息已经丢失。
相对于t来说v是什么
v代表reflect.Value类型。比较有意思的是,通过查看其源码,我们可以看到reflect.Value几乎把reflect.Type的方法重新实现了一遍(比如 NumMethod、Method、NumField)等,只不过其返回不同,在reflect.Value中能返回值为reflect.Value类型,在reflect.Type中能返回值为reflect.Type类型。
获取v所指向对象的方法
通过上面的描述我们可以知道,可以通过与t相似的方法获取v所指向对象的方法,如下:
fmt.Println("value about")fmt.Println(v.NumMethod())fmt.Println(v.Method(v.NumMethod()-1))相应的输出如下:
value about
2
0x1099f60
需要注意,这里的0x1099f60只是输入了方法的reflect.Value类型的地址(因为v.Method(v.NumMethod() - 1)))返回的是一个 reflect.Value类型实例。
动态方法调用
通过t或者v获取到了方法以后,可以通过显示调用Call()函数进行调用,如下:
//无输入参数的方法调用, 构造zero valueargs:=make([]reflect.Value,0)v.MethodByName("SayName").Call(args)对应的输出为:
Jingwei
小结
根据网络上的内容,本文对golang的反射机制进行了简单的探究。对比Ruby中的运行时,golang在运行时各对象角色的获取稍显得复杂。从分析可以简单知道,Golang只能对已有的结构进行反射,无法在运行时创建新的结构;换句话说,Golang语言中的结构在代码编写时便已经决定,无法动态生成,这一点Ruby表现要灵活一些。
参考
- golang 反射
- The Laws of Reflection - The Go Blog Golang官方对反射的解释
- The Laws of Reflection(Go语言反射定律) 同上,但是容易访问
- Ruby元编程 介绍Ruby元编程很经典的著作