简单理解golang中的反射机制

假如把语言的运行时环境看做集市

假如把语言的运行时环境看做集市,在Ruby语言里,你能够在这个集市里认清楚所有的角色。这是Ruby作为动态语言的一个特性,在运行时环境,Ruby保存了对象的所有元数据,所以开发人员能清楚地列出对象的属性、方法等。

那么,Golang作为一种类C语言的静态语言,通过什么样的机制来识别运行时集市中的角色的呢?

角色信息必然保存在某个地方

如果想识别运行时集市中的角色,其角色信息必然保存在某个地方。有了这个信念,问题就变成了:如何在运行时的集市中拿到角色信息。

代码运行环境

代码原型

仿照golang 反射中的做法,我们先给出一个代码约定,后面的代码都是基于下面的代码运行得出的结果:

package main

import (
	"fmt"
	"reflect"
)

type boy struct {
	Name string
	age  int
}

type human interface {
	SayName()
	SayAge()
}

func (b *boy) SayName() {
	fmt.Println(b.Name)
}

func (b *boy) SayAge() {
	fmt.Println(b.age)
}

func main() {
	// 定义接口变量
	var i human
	// 初始化对象,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则打印出了内容值,总结来看就是:

  1. t 表示i接口的当前类型,它指向main包下struct boy的指针类型;
  2. 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.
type rtype struct {
	size       uintptr
	ptrdata    uintptr  // number of bytes in the type that can contain pointers
	hash       uint32   // hash of type; avoids computation in hash tables
	tflag      tflag    // extra type information flags
	align      uint8    // alignment of variable with this type
	fieldAlign uint8    // alignment of struct field with this type
	kind       uint8    // enumeration for C
	alg        *typeAlg // algorithm table
	gcdata     *byte    // garbage collection data
	str        nameOff  // string form
	ptrToThis  typeOff  // 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.
type Method struct {
	// 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_identifiers
	Name    string
	PkgPath string

	Type  Type  // method type
	Func  Value // func with receiver as first argument
	Index int   // 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 value
	args := make([]reflect.Value, 0)
	v.MethodByName("SayName").Call(args)

对应的输出为:

Jingwei

小结

根据网络上的内容,本文对golang的反射机制进行了简单的探究。对比Ruby中的运行时,golang在运行时各对象角色的获取稍显得复杂。从分析可以简单知道,Golang只能对已有的结构进行反射,无法在运行时创建新的结构;换句话说,Golang语言中的结构在代码编写时便已经决定,无法动态生成,这一点Ruby表现要灵活一些。

参考