Go 中的反射模型
Rob Pike
2011 年 09 月 06 日
介绍
反射是指程序检查自身结构的能力,尤其是通过类型;它是一种元编程,也是产生混乱的重要根源。
本文我们通过解释 Go 中反射的工作原理来澄清一些问题。每种语言的反射模型都是不同的(还有许多语言根本不支持反射),但是本文是关于 Go 的,因此后文中提到的“反射”都是指“Go 中的反射”。
类型和接口
因为反射建立在类型系统上,所以让我们从 GO 语言的类型开始。
Go 是静态类型语言。每个变量都有一个静态的类型,即在编译时类型已知且固定:比如 int
, float32
,*MyType
, []byte
等等。如果我们声明
type MyInt int
var i int
var j MyInt
那么 i
的类型是 int
,j
的类型是 MyInt
。变量 i
和 j
具有不同的静态类型,尽管它们有相同的基础类型,但是如果不进行转换,就无法将它们赋值给彼此。
接口类型是类型的一个重要类别,它表示固定的方法集。接口变量可以存储任何具体值(非接口),只要该值实现接口的方法即可。一个典型的例子就是 io 包 的 io.Reader
和 io.Writer
,Reader
和 Writer
类型:
// Reader 是封装基本 Read 方法的接口
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer 是封装基本 Write 方法的接口
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)
// 等等
需要明确的是,不管 r
可能包含什么具体值,r
的类型始终是 io.Reader
:Go 是静态类型的语言,而 r
的静态类型是 io.Reader
。
接口类型的一个非常重要的示例是空接口:
interface{}
它表示空的方法集,并且任何值都满足空接口,因为任何值都有零个或者多个方法。
有人说 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
中包含 (value, type) 对,即(tty
, *os.File
)。请注意,类型 *os.File
实现的方法不只有 Read
;尽管接口仅提供对 Read
方法的访问,但是其内部的值仍包含有关该值的所有类型信息。这就是为什么我们可以做下面的事情:
var w io.Writer
w = r.(io.Writer)
该表达式中的赋值是类型断言。它断言的是 r
中的数据项也实现了 io.Writer
,因此我们可以将其分配给 w
。赋值后,w
中会包含该 (value, type) 对,(tty
, *os.File
),这与 r
中所持有的相同。接口的静态类型决定了接口变量可以调用哪些方法,尽管其内部的具体类型可能有更大的方法集。
我们还可以这样做:
var empty interface{}
empty = w
我们的空接口值 empty
将再次包含同一对 (tty
, *os.File
)。这十分方便,空接口可以保存任何值,并持有我们可能需要的有关该值的所有信息。
(这里我们不需要类型断言,因为已知 w
肯定满足空接口。在实例中,我们将值从 Reader
移到 Writer
时需要显式的类型断言,因为 Writer
的方法集不是 Reader
的方法集的子集。)
一个重要的细节是,接口内始终保存(值,具体类型)形式的元素对,而不会有(值,接口类型)的形式。接口内部不持有接口值。
现在我们可以开始看反射了。
反射第一定律
1. 从接口值反射出反射对象
从底层讲,反射只是一种检查存储在接口变量中的值和类型对的机制。首先,我们需要了解 反射包 的两个类型: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 返回 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
之类的方法,但是要使用它们,我们需要理解 settability,这是反射第三定律的主题,后续再进行讨论。
反射库具有几个值得一提的属性。首先,为了保持API的简单,Value
的 "getter" and "setter" 方法在可以保存该值的最大类型上进行操作,例如所有的的有符号整数都用 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 返回一个 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
方法恢复接口值;实际上,该方法将类型和值信息打包回接口表示形式并返回结果:
//接口返回v的值作为接口{}。
func (v Value) Interface() interface{}
结果,我们可以说
y := v.Interface().(float64) // y的类型为float64
fmt.Println(y)
打印反射对象v
表示的float64
值。
不过,我们可以做得更好。 fmt.Println
,fmt.Printf
等的参数都作为空接口值传递,然后由fmt
包内部解压缩我们在前面的示例中一直在做。因此,正确打印reflect.Value
内容的全部工作就是将Interface
方法的结果传递给格式化的打印例程:
fmt.Println(v.Interface())
(为什么不是fmt.Println(v)
?因为v
是一个reflect.Value
;我们希望它拥有具体的值。)因为我们的值是float64
,如果需要,我们甚至可以使用浮点格式:
fmt.Printf("value is %7.1e.", 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)//错误:会panic错误。
如果您运行此代码,它将因密码错误而惊慌
panic: reflect.Value.SetFloat using unaddressable value
问题不是7.1
值不可寻址;这是v
不可设置的。可设置性是反射Value
的属性,并非所有反射Values
都具有它。
Value
的CanSet
方法报告Value
的可设置性;就我们而言
var x float64 = 3.4
v:= reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
版画
fmt.Println("settability of v:", v.CanSet())
`
在不可设置的Value
上调用Set
方法是错误的。但是什么是可结算性?
Settability 有点像可寻址性,但是更严格。它是反射对象可以修改用于创建反射对象的实际存储的属性。Settability 由反射对象是否持有原始的数据项决定。当我们这样做时
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
却不会受到影响。那将引起混乱并且是无用的,因此这种操作被定为非法的,settability 正是用来避免这种问题的属性。
这实际上是一种很常见的操作。考虑将 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) // 注意:取 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 := 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
反射可能很难理解,通过反射中的类型 Types
和 Values
可能会掩盖正在发生的事情。请记住,反射值需要变量的地址才能修改其表示的值。
结构
在我们之前的示例中,v
本身并不是指针,它只是从一个指针派生的。发生这种情况的常见方法是使用反射修改结构的场。只要有了结构的地址,就可以修改其字段。
这是一个分析结构值t
的简单示例。我们使用结构的地址创建反射对象,因为稍后将要对其进行修改。然后我们将typeOfT
设置为其类型,并使用简单的方法调用对字段进行迭代(有关详细信息,请参见package反射)。请注意,我们从结构类型中提取了字段的名称,但是字段本身是常规的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.", 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}
如果我们修改程序以便从t
而不是&t
创建s
,则对SetInt
和SetString < aaaa>将失败,因为无法设置
t `的字段。
结论
这又是反射定律:
-反射从接口值到反射对象。
-反射从反射对象到接口值。
-要修改反射对象,该值必须可设置。
一旦理解了这些定律,尽管Go中的反射仍然很微妙,但它变得更易于使用。这是一个功能强大的工具,除非绝对必要,否则应谨慎使用并避免使用。
我们还没有涉及到很多反思,包括在通道上发送和接收,分配内存,使用切片和映射,调用方法和函数,但是这篇文章足够长。我们将在以后的文章中介绍其中一些主题。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。