Go 博客
反射三法则
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.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 )接口。
就此而言,若某个值的类型拥有 Read 方法, io.Reader 类型的变量就能保存它:
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{}它表示空方法集。由于任何值都有零个 或多个方法,因此任何值都满足它。
有人说Go的接口是动态类型的,不过这是种误解。它们确实是静态类型的: 接口类型的变量总有着相同的静态类型,就算存储在其中的值的类型在运行时会改变, 它也总是满足该接口。
对于所有的这些,我们都必须严谨对待,因为反射和接口密切相关。
Russ Cox 写过一篇关于 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
而我们的空接口值 e 也将再次包含同样的一对 ( 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
你可能会问接口在哪,因为该程序看起来只是向 reflect.TypeOf 传递了一个
float64 类型的变量 x ,而不是一个接口值。但它就在那,
正如 godoc 所显示的那样, reflect.TypeOf
的签名包含了一个空接口:
// TypeOf 返回 interface{} 中的值的反射类型 Type。
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>
The String method does not.)
(我们显式地调用了 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
的 getter 和 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 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 将 v 的值返回成 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.Println(v) ?因为 v 是个
reflect.Value ,而我们想要的是它保存的具体值。)由于值的类型是
float64 ,如果需要的话,我们甚至可以使用浮点数格式化:
fmt.Printf("value is %7.1e\n", v.Interface())然后就会得到
3.4e+00
再次强调,这里无需将 v.Interface() 的结果类型断言为 float64 ,
因为空接口值中拥有具体值的类型信息,而 Printf 则会将它还原。
简单来说, Interface 方法就是 ValueOf 函数的“反函数”,
不过其结果总是静态类型 interface{} 。
重申一遍:从接口值可反射出反射对象,反之亦可。
反射法则之三
第三条法则是最微妙而令人困惑的,但如果我们从第一条法则开始,还是很容易理解的。
这些代码虽然不能工作,但很值得学习。
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) // Error: will panic.
如果你运行这段代码,它就会报出神秘的恐慌信息:
panic: reflect.Value.SetFloat using unaddressable value
其问题的根源不在于值 7.1 能不能寻址,而在于 v 不可设置。
可设置性是反射值 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) // 注意:获取 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 隐藏了发生的事情。你只要记住反射值需要某些东西的地址来修改它所代表的东西即可。
结构体
在我们前面的例子中, 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中的这些反射法则,它就会变得更容易使用了,尽管它还是很微妙。 这是个强大的工具,因此除非必要,否则应当避免或小心使用。
还有大量关于反射的内容没有涉及到——信道上的发送和接收、分配内存、使用切片和映射、 调用方法和函数等——但这篇文章已经够长了,因此这些话题会在以后的文章中逐一讲解。
相关文章
- HTTP/2 Server Push
- Introducing HTTP Tracing
- Generating code
- Introducing the Go Race Detector
- Go maps in action
- go fmt your code
- 组织 Go 代码
- Debugging Go programs with the GNU Debugger
- The Go image/draw package
- The Go image package
- Error handling and Go
- "First Class Functions in Go"
- Profiling Go Programs
- A GIF decoder: an exercise in Go interfaces
- Introducing Gofix
- Godoc: documenting Go code
- Gobs of data
- C? Go? Cgo!
- JSON and Go
- Go 切片:用法和本质
- Go Concurrency Patterns: Timing out, moving on
- Defer, Panic, and Recover
- Share Memory By Communicating
- JSON-RPC: a tale of interfaces