Go 博客

反射定律

Rob Pike
2011 年 9 月 6 日

简介

计算中的反射是指程序检查自身结构的能力,特别是通过类型进行检查;这是一种元编程形式。它也是一个容易混淆的概念。

在本文中,我们将尝试通过解释 Go 中的反射工作原理来澄清问题。每种语言的反射模型都不同(而且许多语言根本不支持反射),但本文是关于 Go 的,因此在本文的其余部分,"反射"一词应理解为 "Go 中的反射"。

2022 年 1 月补充说明:这篇博文写于 2011 年,早于 Go 中的参数多态性(也称为泛型)。虽然文章中的重要内容并没有因 Go 语言的这一发展而变得不正确,但在一些地方进行了调整,以避免让熟悉现代 Go 的人感到困惑。

类型和接口

由于反射建立在类型系统之上,所以我们先回顾一下 Go 中的类型。

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

type MyInt int

var i int
var j MyInt

那么 i 的类型是 int,而 j 的类型是 MyInt。变量 ij 具有不同的静态类型,尽管它们具有相同的底层类型,但不能在不进行类型转换的情况下相互赋值。

类型中一个重要的类别是接口类型,它表示方法的固定集合。(在讨论反射时,我们可以忽略接口定义在多态代码中用作约束的用法。)接口变量可以存储任何具体的(非接口)值,只要该值实现了接口的方法。一个众所周知的例子是 io.Readerio.Writer,它们是来自 io 包ReaderWriter 类型

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int, err error)
}

任何具有此签名的 Read(或 Write)方法的类型都被认为实现了 io.Reader(或 io.Writer)。就本文的讨论而言,这意味着类型为 io.Reader 的变量可以保存任何类型的值,只要该值的类型具有 Read 方法

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

重要的是要清楚,无论 r 可能保存什么具体的值,r 的类型始终是 io.Reader:Go 是静态类型的,r 的静态类型是 io.Reader

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

interface{}

或其等效的别名

any

它表示方法的空集,并且任何值都能满足,因为每个值都有零个或多个方法。

有些人说 Go 的接口是动态类型的,但这是一种误导。它们是静态类型的:接口类型变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值类型可能发生变化,该值始终会满足接口。

我们需要对所有这些内容保持精确,因为反射和接口密切相关。

接口的表示

Russ Cox 写了一篇关于 Go 中接口值表示的详细博文。这里没有必要重复完整的故事,但需要一个简化的总结。

接口类型变量存储一对:分配给变量的具体值,以及该值的类型描述符。更准确地说,该值是实现接口的底层具体数据项,而类型描述了该项的完整类型。例如,在以下操作之后

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

r 包含,以示意方式,(值,类型) 对,(tty, *os.File)。请注意,类型 *os.File 实现了除 Read 之外的其他方法;即使接口值仅提供对 Read 方法的访问,但内部的值会携带关于该值的所有类型信息。这就是为什么我们可以执行以下操作的原因

var w io.Writer
w = r.(io.Writer)

此赋值中的表达式是一个类型断言;它断言的是 r 内部的项目也实现了 io.Writer,因此我们可以将其赋值给 w。赋值后,w 将包含 (tty, *os.File) 对。这与 r 中保存的对相同。接口的静态类型决定了可以使用接口变量调用的方法,即使内部的具体值可能具有更大的一组方法。

继续,我们可以执行以下操作

var empty interface{}
empty = w

我们的空接口值 empty 将再次包含相同的对,(tty, *os.File)。这很方便:空接口可以保存任何值,并包含我们可能需要了解该值的所有信息。

(我们不需要在这里进行类型断言,因为静态地已知 w 满足空接口。在将值从 Reader 移动到 Writer 的示例中,我们需要显式地使用类型断言,因为 Writer 的方法不是 Reader 的方法的子集。)

一个重要的细节是,接口变量内部的对始终具有 (值,具体类型) 的形式,而不能具有 (值,接口类型) 的形式。接口不保存接口值。

现在我们可以进行反射了。

反射的第一定律

1. 反射从接口值到反射对象。

在基本层面上,反射只是一种机制,用于检查存储在接口变量内部的类型和值对。要开始,我们需要了解 reflect 包 中的两个类型:TypeValue。这两个类型提供了对接口变量内容的访问,还有两个简单的函数,分别称为 reflect.TypeOfreflect.ValueOf,它们从接口值中检索 reflect.Typereflect.Value 部分。(此外,还可以从 reflect.Value 中轻松获取相应的 reflect.Type,但为了保持简洁,我们将暂时将 ValueType 概念分开。)

让我们从 TypeOf 开始

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

此程序输出

type: float64

你可能想知道这里的接口在哪里,因为程序看起来像是将 float64 变量 x 而不是接口值传递给 reflect.TypeOf。但它就在那里;正如 godoc 报告的那样reflect.TypeOf 的签名包含一个空接口

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用 reflect.TypeOf(x) 时,x 首先存储在一个空接口中,然后作为参数传递;reflect.TypeOf 解包该空接口以恢复类型信息。

reflect.ValueOf 函数当然会恢复值(从这里开始我们将省略样板代码,只关注可执行代码)

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

输出

value: <float64 Value>

(我们显式地调用 String 方法,因为默认情况下 fmt 包会深入 reflect.Value 以显示内部的具体值。String 方法不会。)

reflect.Typereflect.Value 都有很多方法,让我们可以检查和操作它们。一个重要的例子是,Value 具有一个 Type 方法,它会返回 reflect.ValueType。另一个例子是,TypeValue 都有一个 Kind 方法,它会返回一个常量,指示存储的项目类型:UintFloat64Slice 等等。此外,Value 上的方法,例如 IntFloat,允许我们获取存储在内部的值(作为 int64float64

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

输出

type: float64
kind is float64: true
value: 3.4

还有 SetIntSetFloat 之类的方法,但要使用它们,我们需要了解可设置性,这是第三个反射定律的主题,将在下面讨论。

反射库具有两个值得特别指出的属性。首先,为了保持 API 的简单性,Value 的 "getter" 和 "setter" 方法对可以容纳该值的最大的类型进行操作:例如,所有带符号整数的 int64。也就是说,ValueInt 方法返回一个 int64,而 SetInt 值采用一个 int64;可能需要将其转换为实际涉及的类型

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint returns a uint64.

第二个属性是,反射对象的 Kind 描述的是底层类型,而不是静态类型。如果反射对象包含用户定义整数类型的的值,例如

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

那么 vKind 仍然是 reflect.Int,即使 x 的静态类型是 MyInt,而不是 int。换句话说,Kind 不能区分 intMyInt,即使 Type 可以。

反射的第二定律

2. 反射从反射对象到接口值。

就像物理反射一样,Go 中的反射也会生成自己的逆运算。

给定一个 reflect.Value,我们可以使用 Interface 方法恢复接口值;实际上,该方法将类型和值信息重新打包到接口表示中,并返回结果

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

因此,我们可以说

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

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

不过,我们可以做得更好。fmt.Printlnfmt.Printf 等等的参数都是作为空接口值传递的,然后由 fmt 包在内部解包,就像我们在前面的示例中所做的那样。因此,要正确打印 reflect.Value 的内容,只需将 Interface 方法的结果传递给格式化的打印例程

fmt.Println(v.Interface())

(自从本文首次发表以来,fmt 包进行了更改,因此它会自动解包像这样的 reflect.Value,因此我们可以直接说

fmt.Println(v)

以获得相同的结果,但为了清楚起见,我们将在本文中保留 .Interface() 调用。)

由于我们的值是 float64,所以我们甚至可以使用浮点格式,如果我们想

fmt.Printf("value is %7.1e\n", v.Interface())

并将得到

3.4e+00

再次说明,没有必要将 v.Interface() 的结果类型断言为 float64;空接口值包含具体值的类型信息,而 Printf 将会恢复它。

简而言之,Interface 方法是 ValueOf 函数的逆运算,除了它的结果始终是静态类型 interface{}

重申:反射从接口值到反射对象,再从反射对象回到接口值。

反射的第三定律

3. 要修改反射对象,该值必须可设置。

第三定律是最微妙、最令人困惑的,但如果我们从基本原理开始,它就很容易理解。

以下是一些无法正常工作的代码,但值得研究。

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你运行这段代码,它将出现一个神秘的消息 "panic":

panic: reflect.Value.SetFloat using unaddressable value

问题不在于值 7.1 不可寻址,而在于 v 不可设置。可设置性是反射 Value 的属性,并非所有反射 Values 都具备此属性。

ValueCanSet 方法报告 Value 的可设置性;在本例中,

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

输出

settability of v: false

在不可设置的 Value 上调用 Set 方法会导致错误。但可设置性是什么呢?

可设置性有点像可寻址性,但更严格。它指的是反射对象是否可以修改用于创建该反射对象的实际存储。可设置性取决于反射对象是否持有原始项目。当我们说

var x float64 = 3.4
v := reflect.ValueOf(x)

我们将 x 的副本传递给 reflect.ValueOf 时,作为 reflect.ValueOf 参数创建的接口值是 x 的副本,而不是 x 本身。因此,如果允许语句

v.SetFloat(7.1)

执行成功,它将不会更新 x,即使 v 看起来像是从 x 创建的。相反,它将更新存储在反射值中的 x 的副本,而 x 本身将不受影响。这将令人困惑且无用,因此是非法的,而可设置性就是用于避免此问题的属性。

如果这看起来很奇怪,其实并非如此。这其实是在不同情况下的一种熟悉情况。想想将 x 传递给函数

f(x)

我们不会期望 f 能够修改 x,因为我们传递的是 x 值的副本,而不是 x 本身。如果我们希望 f 直接修改 x,我们必须将 x 的地址(即指向 x 的指针)传递给我们的函数

f(&x)

这很直观且熟悉,反射的工作方式也是一样的。如果我们希望通过反射修改 x,我们必须将要修改的值的地址提供给反射库。

让我们来做一下。首先,我们像往常一样初始化 x,然后创建一个指向它的反射值,称为 p

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

到目前为止的输出为

type of p: *float64
settability of p: false

反射对象 p 不可设置,但我们想要设置的不是 p,而是(实际上是)*p。要获取 p 指向的内容,我们调用 ValueElem 方法,它通过指针进行间接访问,并将结果保存到名为 v 的反射 Value

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

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

settability of v: true

并且因为它代表 x,我们终于可以使用 v.SetFloat 来修改 x 的值

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

如预期,输出为

7.1
7.1

反射可能难以理解,但它正在做着语言本身正在做的事情,只是通过可以掩盖实际情况的反射 TypesValues。只需记住,反射 Values 需要某物的地址才能修改它们所代表的内容。

结构体

在我们之前的示例中,v 本身并不是一个指针,它只是从一个指针派生的。这种情况出现的一种常见方式是使用反射修改结构体的字段。只要我们拥有结构体的地址,我们就可以修改它的字段。

以下是一个简单的示例,它分析一个结构体值 t。我们使用结构体的地址创建反射对象,因为我们以后会需要修改它。然后,我们将 typeOfT 设置为其类型,并使用简单的函数调用来迭代字段(有关详细信息,请参阅 reflect 包)。请注意,我们从结构体类型中提取了字段的名称,但字段本身是普通的 reflect.Value 对象。

type T struct {
    A int
    B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

该程序的输出为

0: A int = 23
1: B string = skidoo

这里还有关于可设置性的另一个要点:T 的字段名称是大写字母(导出),因为只有结构体的导出字段才是可设置的。

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

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

以下是结果

t is now {77 Sunset Strip}

如果我们修改程序,使 st 创建,而不是从 &t 创建,那么对 SetIntSetString 的调用将失败,因为 t 的字段将不可设置。

结论

以下是反射的定律

  • 反射从接口值到反射对象。

  • 反射从反射对象到接口值。

  • 要修改反射对象,该值必须可设置。

一旦你理解了这些定律,Go 中的反射就会变得容易使用得多,尽管它仍然很微妙。它是一种强大的工具,应该谨慎使用,除非绝对必要。

还有很多关于反射的内容我们没有涵盖——在通道上发送和接收,分配内存,使用切片和映射,调用方法和函数——但本文已经足够长了。我们将在以后的文章中介绍其中的一些主题。

下一篇文章: Go image 包
上一篇文章: 两次 Go 演讲:"Go 中的词法扫描" 和 "Cuddle: 一个 App Engine 演示"
博客索引