go refilect官方文档
介绍
计算机中的反射是指程序通过类型检查来检查其自身的结构的能力;这是一种元编程形式。但它也是一个巨大的困扰。
本文试图通过解释Go语言中的反射如何工作来澄清这些问题。每种语言的反射模型都是不同的(许多语言根本不支持反射),但本文是关于Go语言的,因此在本文的剩余部分中,“反射”这个词应该被理解为“Go语言中的反射”。
注:2022年1月增加:本文是在2011年编写的,比Go语言中的参数化多态(即通用类型)发展要早。虽然由于该语言开发中的发展,本文中的重要内容没有变化,但已在一些地方进行了微调,以避免混淆熟悉现代Go语言的人。
类型和接口
由于反射是建立在类型系统之上的,让我们从Go语言中类型的基础知识开始。
Go语言是静态类型的。每个变量都有一个静态类型,也就是在编译时已知和固定的一个类型:int、float32、*MyType、[]byte等等。如果我们声明
1 | type MyInt int |
那么i的类型为int,j的类型为MyInt。变量i和j具有不同的静态类型,并且尽管它们具有相同的基础类型,但不能在没有转换的情况下互相赋值。
类型的一个重要类别是interface类型,它表示一组固定的方法(当讨论反射时,我们可以忽略在多态代码中使用interface定义作为约束的情况)。只要该值实现了接口的方法,接口变量就可以存储任何具体(非接口)值。一个众所周知的例子是io.Reader和io.Writer,来自io包的Reader和Writer类型:
1 | // Reader is the interface that wraps the basic Read method. |
任何实现具有此签名的Read(或Write)方法的类型都被认为实现了io.Reader(或io.Writer)。对于本讨论而言,这意味着类型为io.Reader的变量可以容纳任何具有Read方法的类型的值:
1 | var r io.Reader |
重要的是要清楚,无论r具体保存什么值,r的类型始终为io.Reader:Go是静态类型语言,r的静态类型是io.Reader。
接口类型的一个极为重要的例子是空接口:
1 | interface{} |
或者等价的别名
1 | any |
any 它表示方法集为空,可以由任何值满足,因为每个值都有零个或多个方法。
有些人说Go的接口是动态类型的,但这是误导人的。它们是静态类型的:接口类型的变量始终具有相同的静态类型,尽管在运行时存储在接口变量中的值可能会更改类型,但该值始终满足接口。
我们需要精确地理解所有这些,因为反射和接口密切相关。
接口的表示形式
Russ Cox在一篇详细的博客文章中讲述了Go中接口值的表示方式。这里不需要重复完整的故事,但是需要提供一个简化的摘要。
接口类型的变量存储了一对值:分配给变量的具体值和该值的类型描述符。更准确地说,该值是实现接口的底层具体数据项,类型描述了该项的完整类型。例如,在执行以下代码后:
1 | var r io.Reader |
r包含模式为(tty
,* os.File
)的(值,类型)对。请注意,类型* os.File
实现了除Read之外的其他方法。即使接口值仅提供对Read方法的访问,内部的值仍携带有关该值的所有类型信息。这就是为什么我们可以做这样的事情:
1 | var w io.Writer |
此分配中的表达式是类型断言。它断言的是r内部的项也实现了io.Writer,因此我们可以将其分配给w。分配后,w将包含(tty
,* os.File
)对。这是与r保存的相同对。接口的静态类型确定了可以使用接口变量调用哪些方法,尽管内部的具体值可能具有更大的方法集。
继续,我们可以这样做:
1 | var empty interface{} |
我们的空接口值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 | package main |
该程序打印出:
1 | type: float64 |
你可能会想知道这里的接口在哪里,因为程序看起来像是将 float64 变量 x 而不是接口值传递给 reflect.TypeOf。但它是存在的;正如 godoc 所报告的那样,reflect.TypeOf 的签名包括一个空接口:
1 | // TypeOf 返回接口{}中的值的反射类型。 |
当我们调用 reflect.TypeOf(x) 时,x 首先被存储在一个空接口中,然后作为参数传递;reflect.TypeOf 拆开该空接口以恢复类型信息。
当然,reflect.ValueOf 函数恢复值(从现在开始,我们将省略样板代码并专注于可执行代码):
1 | var x float64 = 3.4 |
打印:
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 | var x float64 = 3.4 |
打印:
1 | type: float64 |
还有像SetInt和SetFloat这样的方法,但要使用它们,我们需要了解可设置性,这是反射的第三条定律的主题,下面将进行讨论。
反射库有一些值得关注的属性。首先,为了保持API的简单性,Value的“getter”和“setter”方法都操作可以容纳该值的最大类型:例如,对于所有有符号整数,Int方法返回int64,SetInt方法接受int64;可能需要将其转换为实际涉及的类型:
1 | var x uint8 = 'x' |
第二个属性是,反射对象的Kind描述的是底层类型,而不是静态类型。例如,如果反射对象包含一个用户定义的整数类型的值,如
1 | type MyInt int |
则v的Kind仍然是reflect.Int,即使x的静态类型是MyInt而不是int。换句话说,Kind无法区分int和MyInt,即使Type可以。
第二反射定律–反射可以从反射对象到接口值。
与物理反射一样,Go中的反射生成了它自己的逆反射。
给定一个reflect.Value,我们可以使用Interface方法恢复一个接口值。实际上,该方法将类型和值信息打包回一个接口表示中,并返回结果:
1 | // Interface将v的值作为接口{}返回 |
因此,我们可以说:
1 | y := v.Interface().(float64) // y的类型将是float64 |
以打印由反射对象v表示的float64值。
但我们甚至可以做得更好。fmt.Println
、fmt.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 | var x float64 = 3.4 |
如果运行此代码,它将以神秘的消息引发panic:
1 | panic: reflect.Value.SetFloat using unaddressable value |
问题不在于值7.1不可寻址;问题在于v不可设置。Settability是反射Value的属性,并非所有反射Values都具有Settability。
Value的CanSet方法报告值的可设置性;在我们的情况下,
1 | var x float64 = 3.4 |
打印:
1 | settability of v: false |
在不可设置的Value上调用Set方法是错误的。但是,可设置性是什么?
可设置性有点像可寻址性,但更严格。这是反射对象可以修改用于创建反射对象的实际存储的属性。可设置性由反射对象是否持有原始项来确定。当我们说
1 | var x float64 = 3.4 |
我们将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 | var x float64 = 3.4 |
到目前为止的输出是
1 | type of p: *float64 |
反射对象p不可设置,但我们要设置的不是p,而是(实际上)*p
。为了到达p指向的位置,我们调用Value的Elem方法,该方法通过指针进行间接引用,并将结果保存在一个反射Value中,称为v:
1 | v := p.Elem() |
现在,v是一个可设置的反射对象,正如输出所示,
1 | settability of v: true |
并且由于它代表x,我们最终能够使用v.SetFloat来修改x的值:
1 | v.SetFloat(7.1) |
预期的输出是
1 | 7.1 |
反射可能很难理解,但它正在做语言所做的事情,尽管是通过反射类型和值来伪装正在发生的事情。
只需记住,反射值需要某个东西的地址,以便修改它们所代表的内容。
结构体
在我们之前的示例中,v本身不是一个指针,它只是从一个指针派生而来。这种情况经常出现的一种方式是使用反射来修改结构体的字段。只要我们有结构体的地址,就可以修改其字段。
下面是一个简单的示例,分析一个结构体值t。我们使用结构体的地址创建反射对象,因为之后我们将要修改它。然后我们将typeOfT
设置为它的类型,并使用直接的方法调用(有关详细信息,请参见[reflect包](https://go.dev/pkg/reflect/)迭代字段。请注意,我们从结构体类型中提取字段的名称,但字段本身是常规的reflect.Value对象。
1 | type T struct { |
这个程序的输出是
1 | 0: A int = 23 |
这里还有一个关于可设置性的点:T的字段名是大写的(导出的),因为只有结构体的导出字段是可设置的。
(否则可能错误panic: reflect.Value.Interface: cannot return value obtained from unexported field or method
)
因为s包含一个可设置的反射对象,所以我们可以修改结构体的字段。
1 | s.Field(0).SetInt(77) |
下面是结果:
1 | t is now {77 Sunset Strip} |
如果我们修改程序,使s是从t而不是&t创建的,则对SetInt和SetString的调用将失败,因为t的字段将无法设置。
结论
以下是反射的规则:
- 反射从接口值转换为反射对象。
- 反射从反射对象转换为接口值。
- 要修改反射对象的值,该值必须是可设置的。
一旦理解了这些规则,Go 中的反射就变得更加容易使用,尽管它仍然很微妙。它是一个强大的工具,应该小心使用,并且除非绝对必要,否则应该避免使用。
反射还有很多我们没有涉及的内容 —— 在通道上发送和接收,分配内存,使用切片和映射,调用方法和函数 —— 但是这篇文章已经够长了。我们将在以后的文章中涵盖其中的一些主题。