原创声明


序言

计算机中的反射是程序检查它自己的结构的能力,尤其是通过类型;它是元编程的一种形式。它也是一个主要的难点。

在这篇文章中,我们试图通过阐明 Go 语言中反射的工作原理的方式讲清楚反射。每门编程语言的反射模型都不同(而且很多语言根本不支持反射),这篇文章是关于 Go 的,因此在本文的剩余部分中,“反射”一词等价于“Go 中的反射”。


类型和接口

因为反射构建在类型系统之上,让我们从复习 Go 中的类型开始。

Go 是静态类型的。每个变量都有静态的类型,也就是说,在编译时每个变量都有已知并且确定的类型:int、float32、*MyType、[]byte 等。如果我们声明

那么 i 的类型是 int,j 的类型是 MyInt。变量 i 和 j 拥有不同的静态类型。即使它们拥有相同的底层类型,但是在不转换的情况下,它们不能互相赋值。

接口类型是一个重要的类型种类,它表示固定的方法集合。接口变量可以存储任何实现了接口的方法的具体(非接口)值。io 包中定义的 Reader 和 Writer 类型,io.Reader 和 io.Writer 是一对著名的例子:

可以说任何实现了包括该签名的 Read(或 Write)方法的类型都实现了 io.Reader(或 io.Writer)。这意味着 io.Reader 类型的变量能持有任何类型拥有 Read 方法的值:

必须讲清楚的是不管 r 持有的具体值是什么,r 的类型永远是 io.Reader:Go 是静态类型的,r 的静态类型是 io.Reader。

接口类型的一个非常重要的例子是空接口:

它表示空的方法集合,并且被任何值满足,因为任何值都拥有 0 个或多个方法。

有人说 Go 的接口是动态类型的,这是误导。接口是静态类型的:接口类型的变量永远有相同的静态类型,即便在运行时接口变量中存储的值可能改变类型,但是该值将始终满足接口。

因为反射与接口紧密相关,所以我们需要弄明白这些。


接口的表示

Russ Cox 已经写了一篇关于 Go 中接口值的表示的详细博文。在这里没必要重复所有叙述,但是简短的总结很合时宜。

接口类型的变量存储一个(value,type)对:被赋值给变量的具体值,和该值的类型描述。更准确地说,value 是实现了接口的底层具体数据项,type 描述该数据项的完整类型(full type)。例如

r 大略包含(value,type)对,(tty,*os.File)。注意 *os.File 类型实现了 Read 之外的方法;虽然接口值只提供了对 Read 方法的访问,但是接口值的内部拥有关于具体值的全部类型信息。所以我们可以这样做:

此赋值中的表达式是类型断言(type assertion);它断言的是 r 里面的数据项也实现了 io.Writer,因此我们可以把它赋值给 w。在赋值之后,w 将包含(tty,*os.File)对,它就是 r 中持有的(value,type)对。虽然接口变量里面的具体值可能拥有更大的方法集合,但是接口的静态类型决定可以调用接口变量的哪些方法。

继续,我们可以这样做:

空接口值 empty 将包含相同的(value,type)对,(tty,*os.File)。空接口可以持有任何值,并包含我们可能需要的关于该值的全部信息。

(在这里,我们无需类型断言,因为通过静态分析可知 w 满足空接口。在把来自 Reader 的值赋给 Writer 的例子中,我们需要显式地使用类型断言,因为 Writer 的方法不是 Reader 的方法的子集。)

接口里面的(value,type)对的形式永远是(value,具体类型),不可能是(value,接口类型)。接口不能持有接口值。

现在我们开始讲解反射。


第一反射定律

1,从接口值到反射对象的反射(1. Reflection goes from interface value to reflection object.)

在底层,反射只是一种检查类型和存储在接口变量中的(value,type)对的机制。在开始之前,我们需要先了解 reflect 包中的两种类型:TypeValue。这两种类型提供了访问接口变量的内容的途径,另外还提供了两个简单的函数:

这两个函数分别获取接口值的 reflect.Type 和 reflect.Value 部分。(另外,从 reflect.Value 可以很容易地获取 reflect.Type,但是现在我们把 Value 和 Type 这两个概念分开。)

我们从 TypeOf 开始:

该程序打印

你可能想知道接口在哪里,因为该程序看起来是把 float64 类型的变量 x,而不是接口值,传递给了 reflect.TypeOf。但是 reflect.TypeOf 的签名包含空接口:

当我们调用 reflect.TypeOf(x) 时,x 首先被存储到空接口中,然后作为参数被传递给 reflect.TypeOf;之后 reflect.TypeOf “拆箱”(unpack),重获类型信息。

当然,reflect.ValueOf 函数可以重获该值(从这里起,我们省略模版,仅聚焦于可执行代码):

打印

(我们显式地调用 String 方法是因为默认情况下,fmt 包会钻进 reflect.Value,展示里面的具体值。而 String 方法不会。)

reflect.Type 和 reflect.Value 都有很多供我们查看和使用的方法。比如:

打印

Value 上还有像 SetInt、SetFloat 之类的方法,但是使用它们之前,需要理解可设置性(settability),这是第三反射定律的主题,下面会讨论。

反射库有两个特性值得单独指出。首先为了保持 API 简单,Value 上的“getter”和“setter”方法操作的是能够持有该值的最大类型:比如,int64 用于所有有符号整数。也就是说,Value 的 Int 方法返回 Int64,SetInt 接收 int64;在必要时可以转换回实际类型:

第二个特性是反射对象的 Kind 方法描述底层类型,而不是静态类型。假如反射对象包含用户自定义的整形值

虽然 x 的静态类型是 MyInt,而不是 int,但是 v 的 Kind 仍然是 reflect.Int。换句话说,使用 Kind 无法区分 int 和 MyInt,但通过 Type 可以。


第二反射定律

2,从反射对象到接口值的反射(2. Reflection goes from reflection object to interface value.)

就像物理学上的反射,Go 中的反射也能产生自己的逆反射。

给定一个 reflect.Value,我们使用 Interface 方法可以重新获取接口值;事实上,该方法把类型和值信息“装”(pack)进接口的表示,然后返回结果:

因此我们可以通过下面的操作

来打印被反射对象 v 表示的 float64 值。

我们可以做的更好一些。fmt.Println、fmt.Printf 等方法的参数是空接口值。正如我们在前面的例子中所做的那样,fmt 包内部会把参数“拆箱”(unpack)。因此为了正确地打印 reflect.Value,我们只需将 Interface 方法的结果传递给格式化打印程序:

(为什么不是 fmt.Println(v)?因为 v 是一个 reflect.Value 值;我们想打印它持有的具体值),因为该值的类型是 float64,我们甚至可以使用浮点格式:

在这个例子中会得到

无需使用类型断言(type assertions)将 v.Interface() 的结果转换成 float64,空接口值里面包含具体值的类型信息,Printf 会重新获取它。

简而言之,Interface 方法是 ValueOf 函数的逆过程,除了它的结果永远属于静态类型 interface{}。

重申一遍:反射就是从接口值到反射对象,再返回来。


第三反射定律

3,如果想要修改反射对象,那么值必须是可设置的(settable)(3. To modify a reflection object, the value must be settable.)

第三定律是最难理解的,但是如果我们从第一法则开始,就可以很容易的理解它。

下面是一些无法运行,但值得学习的代码。

运行这段代码会引发带有晦涩难懂的信息的 panic

问题的根源不是值 7.1 不可寻址;而是 v 不可设置。可设置性(Settability)是 Value 的一个属性,并且不是所有 Value 都拥有它。

Value 的 CanSet 方法用于报告 Value 的可设置性;在我们的例子中,

打印

在不可设置的 Value 上调用 Set 方法是错误的。那么什么是可设置性?

与可寻址(addressability)类似,可设置性是一个位(bit),但是很严格。它是一种反射对象能修改用于创建反射对象的实际存储的特性。可设置性由反射对象是否持有原始数据项决定。当我们执行

我们把 x 的副本传递给 reflect.ValueOf。所以作为 reflect.ValueOf 的参数的接口值是从 x 的副本创建的,而不是 x 本身。因此,下面的语句

如果被允许成功,即使 v 看起来像是从 x 创建的,但是它不会更新 x。它将更新存储在反射值内部的 x 的副本,但是 x 本身不受影响。那将令人难以理解,并且无用,因此它是非法的,可设置性是用于避免该问题的特性。

这看起来很奇怪,但是其实不奇怪。这其实是一种披着不同寻常的外衣的常见情况。假设把 x 传递给一个函数:

我们不希望 f 能修改 x,因为我们传递的是 x 的副本,而非 x 本身。如果我们想让 f 直接修改 x,我们必须把 x 的地址(也就是,指向 x 的指针)传递给函数:

这很易懂且常见,反射也是这样。如果我们想通过反射修改 x,那么我们必须把想要修改的值的指针传递给反射库。

接下来我们就这样做。首先我们像平时一样初始化 x,然后创建指向它的反射值,称之为 p。

当前的输出是

反射对象 p 是不可设置的,但是我们想设置的不是 p,而是 *p。我们调用 Value 的 Elem 方法获取 p 指向的东西,称之为 v,然后间接地通过指针,把结果存储到 Value 中:

正如输出所示,v 是可设置的反射对象

因为它表示 x,我们最终可以使用 v.SetFloat 来修改 x 的值:

果不其然,输出是

反射理解起来很困难,尽管它通过 Type 和 Value 掩饰所做的事情,但是它的功能和编程语言完全一样。请记住如果想要修改 Value 表示的数据项,需要给 Value 提供数据项的地址。


结构体

在前面的例子中,v 本身不是指针,它只是派生于指针。当使用反射修改结构体的字段时,这是一种常用方式。只要我们有结构体的地址,我们就能修改它的字段。

下面是一个分析结构体值 t 的简单示例。我们使用结构体的地址创建反射对象是因为我们想要修改它。然后我们把 typeOfT 设置为它的类型,接着使用简单的方法调用迭代结构体的字段(详见 reflect 包)。注意我们从结构体类型提取字段的名称,但是字段本身是常规的 reflect.Value 对象。

这段程序的输出是

在这里顺便介绍另外一个关于可设置性的知识点:T 的字段名的首字母是大写字母(可导出的),因为只有可导出的结构字段是可设置的。

因为 s 包含可设置的反射对象,所以我们可以修改这个结构体的字段。

结果如下:

如果我们将程序需改成从 t,而不是 &t 创建 s,那么对 SetInt 和 SetString 的调用会失败,因为 t 的字段不是可设置的。


总结

反射定律如下:

只要你理解了这些定律,即便反射仍然很微妙,但是在 Go 中使用反射会更容易。反射是一个很强大的工具,但是使用时应该多加小心,并且除非十分需要,否则应该避免使用反射。

还有许多我们没有讲解的反射 — 在通道上发送和接收,申请内存,使用切片和映射,调用方法和函数 — 但是这篇博文已经足够长了。在以后的文章中,我们将讲解上面提到的某些主题。