什么是反射
反射?光的反射吗?在物理学中有反射这个概念,表示光在分界面上改变传播方向又返回原来物质中的现象。本文讲述的是反射是在计算机领域的概念,在计算机科学中,反射编程(reflective programming)定义如下,这是来做维基百科的描述。
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
翻译过来是说,反射编程或反射是指程序在运行时,可以检查、访问和修改它结构(内存布局)或行为的一种能力。简单来说就是在程序运行的过程中访问和修改它自己。具体来说,可执行程序是由数据+指令构成的,修改它的结构即修改数据,对应到源代码中就是修改数据结构和变量内容,修改它的行为即修改指令,对应到源代码中就是修改调用函数。
想一想,如果是让你实现这个反射功能怎么做呢?也许有读者会说,那可以这样做,要反射程序A,我直接写个程序在程序A运行的过程中直接修改他的内存数据和指令,确实可以,不过难度很高,这是黑客玩的。对于我们来说,希望有一种技术机制能够通过编程来实现,在Golang中,标准库提供了reflect包,该包提供了API让我们可以很方便进行反射编程。
为什么需要反射
- 反射能够让程序动态的适应不同的情况,让程序更具通用能力
也许有读者说,用接口也可以呀,确实将参数定义成接口,给接口传不同的实参,就能够动态的调用对应的函数。面向接口编程确实可以以简化编写分别适用于多种不同情形的功能代码,但是反射可以解决比面向接口编程更加普通的场景,下面举一个例子。
有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候这些类型可能还不存在。在fmt库中有一个Sprint函数,fmt.Sprint入参是一个interface{}可变参数,返回一个string对象,它可以用来对任意类型的值进字符串输出,甚至是用户自定义的类型。下面我们也来尝试实现一个类似功能的函数,这里简化处理,只接受一个参数不支持可变参数,然后返回一个string。
type stringer interface {
String() string
}
func MySprint(a interface{}) string {
switch a := a.(type) {
case stringer:
return a.String()
case string:
return a
case int:
return strconv.Itoa(a)
case int8:
return strconv.Itoa(int(a))
case bool:
if a {
return "true"
} else {
return "false"
}
default:
return "<unknown>"
}
}
如果传入的类型是 int16, int32, int64, []int, map[string]string等等类型呢?
我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何
处理类似于url.Values这样的具体的类型呢?url.Values类型定义如下:
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string
Values是基于map[string][]string创建的新类型,它和map[string][]string是不同的类型,底层的元数据信息是不同的。在<go interface 知多少>有讲
也就是说url.Values并不能匹配map[string][]string类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。
没有办法来检查未知类型了, 但是使用反射就能搞定这种情况,这是为什么需要反射的原因。
反射是怎么实现的
根据反射编程的定义,是在运行时能够检查、访问和修改对象的内存布局、调用方法的行为。这里的对象是泛指的概念,可以是内置类型变量,可以是自定义结构体变量,也可以是函数变量。在计算机中,
对一个对象我们可以从类型+值两个维度去描述它。对一个结构体对象来说,类型描述了结构体的名称,它有哪些字段,有哪些方法,对齐方式等等,对一个函数对象来说,类型需要描述函数名称,函数的入参信息,
函数的出参信息等。值让我们可以有途径修改它的内容,修改它的内存布局,方法地址等等。Golang的reflect包正是提供了一系列函数和方法API,可以方便的获取对象的类型和修改它的值。
这里可以读者稍做停顿,想一想,如果是我自己来设计reflect的API,我该怎么做? 根据上面的介绍,对任意一个对象,可以获取到它的类型和值描述性,所以要定义两个函数,签名如下
func typeOf(i interface{}) Type{
...
}
func valueOf(i interface{}) Value{
...
}
两个函数的入参要定义成interface{}类型,因为只有定义成interface{}类型,才可以接受任意类型的参数。反会值Type和Value分别描述类型和值信息。
下面分析reflect包,从源码的角度分析反射的实现。说明,下面的分析基于的go版本是1.14
reflect包两个核心结构是reflect.Type和reflect.Value,对应到上面就是从类型+值去描述一个对象,这两种类型都是可以导出,在外面可以使用它们,构建反射编程。
reflect.Type
reflect.Type是一个接口类型,定义了一系列函数从各个方面去描述对象的类型信息,例如结构体的名称是什么,占用的大小是多少,有多个个字段。该接口一共定义了
31个方法,其中29个可导出类型,另外2个是不可导出,是包内使用的。在29个可导出类型中,可以分为两部分,一部分是对所有的类型都能使用的方法(下表中标注的通用),另一部是的方法是有适用范围的,只有某些甚至某个类型才能调用,不正确的类型调用将引发panic。具体类型说明下表。
函数名 | 是否可导出 | 函数适用的类型 | 说明 |
---|---|---|---|
Align | 是 | 通用 | 类型占用内存大小 |
FieldAlign | 是 | 通用 | 返回该类型作为结构体字段时占用的内存大小 |
Method | 是 | 通用 | 返回第i个方法 |
MethodByName | 是 | 通用 | 返回给定名称的方法 |
NumMethod | 是 | 通用 | 可导出方法的数量 |
Name | 是 | 通用 | 返回包内部类型的名称 |
PkgPath | 是 | 通用 | 返回定义类型的包的路径 |
Size | 是 | 通用 | 返回该类型的值占用的字节数 |
String | 是 | 通用 | 返回类型的字符串表示 |
Kind | 是 | 通用 | 返回该类型的具体类型,每种类型都有唯一的kind值 |
Implements | 是< |