介绍

计算机中的反射是指程序通过类型检查来检查其自身的结构的能力;这是一种元编程形式。但它也是一个巨大的困扰。

本文试图通过解释Go语言中的反射如何工作来澄清这些问题。每种语言的反射模型都是不同的(许多语言根本不支持反射),但本文是关于Go语言的,因此在本文的剩余部分中,“反射”这个词应该被理解为“Go语言中的反射”。

注:2022年1月增加:本文是在2011年编写的,比Go语言中的参数化多态(即通用类型)发展要早。虽然由于该语言开发中的发展,本文中的重要内容没有变化,但已在一些地方进行了微调,以避免混淆熟悉现代Go语言的人。

类型和接口

由于反射是建立在类型系统之上的,让我们从Go语言中类型的基础知识开始。

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

1
2
3
4
type MyInt int

var i int
var j MyInt

那么i的类型为int,j的类型为MyInt。变量i和j具有不同的静态类型,并且尽管它们具有相同的基础类型,但不能在没有转换的情况下互相赋值。

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

1
2
3
4
5
6
7
8
9
// 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方法的类型的值:

1
2
3
4
5
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。

接口类型的一个极为重要的例子是空接口:

1
interface{}

或者等价的别名

1
any

any 它表示方法集为空,可以由任何值满足,因为每个值都有零个或多个方法。

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

我们需要精确地理解所有这些,因为反射和接口密切相关。

接口的表示形式

Russ Cox在一篇详细的博客文章中讲述了Go中接口值的表示方式。这里不需要重复完整的故事,但是需要提供一个简化的摘要。

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

1
2
3
4
5
6
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方法的访问,内部的值仍携带有关该值的所有类型信息。这就是为什么我们可以做这样的事情:

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

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

继续,我们可以这样做:

1
2
var empty interface{}
empty = w

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

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

一个重要的细节是,接口变量中的成对值始终采用(值,具体类型)的形式,而不可能采用(值,接口类型)的形式。接口不能保存接口值。

现在我们准备使用反射。

第一条反射定律–反射从接口值到反射对象。

在基本层次上,反射只是一种检查存储在接口变量内的类型和值对的机制。要开始,我们需要了解 reflect 包中的两种类型:Type 和 Value。这两种类型提供了访问接口变量内容的途径,而两个简单的函数 reflect.TypeOf 和 reflect.ValueOf 则从接口值中检索 reflect.Type 和 reflect.Value 组件。 (此外,从 reflect.Value 很容易到达相应的 reflect.Type,但是让我们现在将 Value 和 Type 的概念分开。)

让我们从 TypeOf 开始:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"reflect"
)

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

该程序打印出:

1
type: float64

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

1
2
// TypeOf 返回接口{}中的值的反射类型。 
func TypeOf(i interface{}) Type

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

当然,reflect.ValueOf 函数恢复值(从现在开始,我们将省略样板代码并专注于可执行代码):

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

打印:

1
value: <float64 Value>

(我们显式调用 String 方法,因为默认情况下 fmt 包会深入 reflect.Value 显示其中的具体值。String 方法则不会这样做。)

reflect.Type 和 reflect.Value 都有很多方法,让我们可以检查和操作它们。

一个重要的例子是,Value 有一个 Type 方法,返回 reflect.Value 的 Type。

另一个例子是,Type 和 Value 都有一个 Kind 方法,返回一个常量,指示存储的项目类型:Uint,Float64,Slice 等等。

也有类似 Int 和 Float 的方法,可以让我们抓取存储的值(作为 int64 和 float64):

1
2
3
4
5
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())

打印:

1
2
3
type: float64
kind is float64: true
value: 3.4

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

反射库有一些值得关注的属性。首先,为了保持API的简单性,Value的“getter”和“setter”方法都操作可以容纳该值的最大类型:例如,对于所有有符号整数,Int方法返回int64,SetInt方法接受int64;可能需要将其转换为实际涉及的类型:

1
2
3
4
5
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描述的是底层类型,而不是静态类型。例如,如果反射对象包含一个用户定义的整数类型的值,如

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

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

第二反射定律–反射可以从反射对象到接口值。

与物理反射一样,Go中的反射生成了它自己的逆反射。

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

1
2
// Interface将v的值作为接口{}返回
func (v Value) Interface() interface{}

因此,我们可以说:

1
2
y := v.Interface().(float64) //  y的类型将是float64
fmt.Println(y)

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

但我们甚至可以做得更好。fmt.Printlnfmt.Printf等的参数都作为空接口值传递,它们随后在fmt包内部进行解包,就像我们在前面的例子中所做的那样。因此,正确打印reflect.Value内容所需的全部步骤就是将Interface方法的结果传递给格式化打印例程:

1
fmt.Println(v.Interface())

(由于go的版本更新,fmt包发生了变化,因此它会自动解包像这样的reflect.Value,因此我们可以只写

1
fmt.Println(v)

得到相同的结果,但为了清晰起见,我们在此处保留.Interface()调用。)

由于我们的值是float64,因此如果需要,我们甚至可以使用浮点格式:

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

并在这种情况下得到:

3.4e+00

再次强调,不需要对v.Interface()的结果进行类型断言为float64;空接口值内部具有具体值的类型信息,并且Printf将恢复它。

简而言之,Interface方法是ValueOf函数的反转,除了其结果始终是静态类型interface{}之外。

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

第三条反射定律–要修改反射对象,必须设置值。

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

下面是一些不起作用但值得学习的代码。

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

如果运行此代码,它将以神秘的消息引发panic:

1
panic: reflect.Value.SetFloat using unaddressable value

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

Value的CanSet方法报告值的可设置性;在我们的情况下,

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

打印:

1
settability of v: false

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

可设置性有点像可寻址性,但更严格。这是反射对象可以修改用于创建反射对象的实际存储的属性。可设置性由反射对象是否持有原始项来确定。当我们说

1
2
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传递给函数:

1
f(x)

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

1
f(&x)

这很简单和熟悉,反射也是同样的道理。如果我们想通过反射修改x,我们必须向反射库提供一个指向要修改的值的指针。

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

1
2
3
4
var x float64 = 3.4
p := reflect.ValueOf(&x) //注意:取x的地址
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

到目前为止的输出是

1
2
type of p: *float64
settability of p: false

反射对象p不可设置,但我们要设置的不是p,而是(实际上)*p。为了到达p指向的位置,我们调用Value的Elem方法,该方法通过指针进行间接引用,并将结果保存在一个反射Value中,称为v:

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

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

1
settability of v: true

并且由于它代表x,我们最终能够使用v.SetFloat来修改x的值:

1
2
3
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

预期的输出是

1
2
7.1
7.1

反射可能很难理解,但它正在做语言所做的事情,尽管是通过反射类型和值来伪装正在发生的事情。

只需记住,反射值需要某个东西的地址,以便修改它们所代表的内容。

结构体

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

下面是一个简单的示例,分析一个结构体值t。我们使用结构体的地址创建反射对象,因为之后我们将要修改它。然后我们将typeOfT设置为它的类型,并使用直接的方法调用(有关详细信息,请参见[reflect包](https://go.dev/pkg/reflect/)迭代字段。请注意,我们从结构体类型中提取字段的名称,但字段本身是常规的reflect.Value对象。

1
2
3
4
5
6
7
8
9
10
11
12
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())
}

这个程序的输出是

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

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

(否则可能错误panic: reflect.Value.Interface: cannot return value obtained from unexported field or method)

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

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

下面是结果:

1
t is now {77 Sunset Strip}

如果我们修改程序,使s是从t而不是&t创建的,则对SetInt和SetString的调用将失败,因为t的字段将无法设置。

结论

以下是反射的规则:

  • 反射从接口值转换为反射对象。
  • 反射从反射对象转换为接口值。
  • 要修改反射对象的值,该值必须是可设置的。

一旦理解了这些规则,Go 中的反射就变得更加容易使用,尽管它仍然很微妙。它是一个强大的工具,应该小心使用,并且除非绝对必要,否则应该避免使用。

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

原文

https://go.dev/blog/laws-of-reflection