Go 官方博客
反射的规律
引言
在计算领域,反射是指程序检查自身结构(特别是通过类型)的能力;它是一种元编程形式。反射也是一个很大的困惑来源。
在本文中,我们将通过解释 Go 中的反射如何工作来澄清问题。每种语言的反射模型都不同(而且许多语言根本不支持反射),但本文是关于 Go 的,因此在本文的其余部分,“反射”一词应理解为“Go 中的反射”。
2022 年 1 月补充说明:这篇博文写于 2011 年,早于 Go 中的参数化多态(也称为泛型)。虽然语言的这一发展并未导致文章中的重要内容出错,但在某些地方进行了修改,以避免混淆熟悉现代 Go 的读者。
类型和接口
由于反射构建于类型系统之上,让我们先回顾一下 Go 中的类型。
Go 是静态类型的语言。每个变量都有一个静态类型,即在编译时已知且固定的精确类型:int
, float32
, *MyType
, []byte
等等。如果我们声明
type MyInt int
var i int
var j MyInt
那么 i
的类型是 int
,j
的类型是 MyInt
。变量 i
和 j
具有不同的静态类型,尽管它们具有相同的底层类型,但不能在不进行转换的情况下相互赋值。
一种重要的类型类别是接口类型,它代表固定的方法集合。(在讨论反射时,我们可以忽略接口定义在多态代码中作为约束的使用。)只要某个具体值实现了接口的方法,一个接口变量就可以存储该值。一个众所周知的例子是 io.Reader
和 io.Writer
,它们是 io 包中的 Reader
和 Writer
类型。
// 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 包中的两个类型:Type 和 Value。这两个类型提供了访问接口变量内容的能力,而两个简单的函数,reflect.TypeOf
和 reflect.ValueOf
,可以从接口值中提取 reflect.Type
和 reflect.Value
部分。(此外,从 reflect.Value
很容易获取相应的 reflect.Type
,但现在让我们暂时将 Value
和 Type
的概念分开。)
让我们从 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.Type
和 reflect.Value
都有许多方法,使我们能够检查和操作它们。一个重要的例子是 Value
有一个 Type
方法,用于返回 reflect.Value
的 Type
。另一个例子是 Type
和 Value
都有一个 Kind
方法,返回一个常量,表示存储的是哪种类型的项:Uint
、Float64
、Slice
等等。此外,Value
上还有像 Int
和 Float
这样的方法,让我们能够获取其中存储的值(以 int64
和 float64
的形式):
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
还有像 SetInt
和 SetFloat
这样的方法,但要使用它们,我们需要理解可设置性,这是反射第三定律的主题,将在下面讨论。
反射库有几个值得单独指出的特性。首先,为了保持 API 的简单性,Value
的“获取”和“设置”方法操作的是能够容纳该值的最大类型:例如,所有有符号整数都使用 int64
。也就是说,Value
的 Int
方法返回一个 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)
v
的 Kind
仍然是 reflect.Int
,即使 x
的静态类型是 MyInt
,而不是 int
。换句话说,Kind
无法区分 int
和 MyInt
,尽管 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.Println
、fmt.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
的一个属性,并非所有反射 Value
都具备此属性。
Value
的 CanSet
方法报告 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
指向的内容,我们调用 Value
的 Elem
方法,该方法通过指针进行间接访问,并将结果保存在一个名为 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
反射可能难以理解,但它所做的正是语言本身所做的事情,尽管是通过可能掩盖实际情况的反射 Type
和 Value
来完成的。只需记住,反射 Value
需要某个东西的地址才能修改它们所代表的内容。
结构体
在之前的例子中,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}
如果我们修改程序,使得 s
是从 t
而不是 &t
创建的,那么对 SetInt
和 SetString
的调用将失败,因为 t
的字段将不可设置。
结论
重申一下反射的定律:
-
反射从接口值到反射对象。
-
反射从反射对象到接口值。
-
要修改反射对象,该值必须是可设置的。
一旦理解了这些定律,Go 中的反射就会变得更容易使用,尽管它仍然微妙。它是一个强大的工具,应谨慎使用,除非绝对必要,否则应避免使用。
反射还有很多我们未涵盖的内容——在 channel 上发送和接收、分配内存、使用切片和 map、调用方法和函数等等——但这篇文章已经够长了。我们将在后面的文章中介绍其中的一些主题。