这是我翻译的国外的一篇文章,原文: https://blog.golang.org/laws-of-reflection

介绍

在计算中的反射是程序检测自己结构的一种能力,尤其是通过类型来检测。它是元编程的一种形式,同时也会造成很大的困惑。

本篇文章,我将会尝试解释 Go 中的反射是如何工作的。每一种语言都有不同的反射模型(有些语言甚至不支持反射),但是本篇文章是关于 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 有相同的底层类型,但是他们有着不同的静态类型,它们不能在没有转换的情况下直接赋值给对方。

类型系统中有一个重要的类型,接口,它代表着固定方法的集合。一个接口变量就像一个盒子一样,可以包裹存储任何具体的值(非接口类型的),只要这个值实现了该接口的方法。一个众所周知的例子是 io.Reader 和 io.Writer, Reader 和 Writer 类型都是来自于 io 包:

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)
// 等等

需要说明的是,无论变量 r 持有什么值,r 的类型总是 io.Reader,因为 Go 是静态类型语言,r 的静态类型是 io.Reader。

关于接口类型的另外一个重要的例子是空接口:

1
interface{}

它代表着一个空的方法集,任何类型的值都满足,因为任何类型的值都有零个或零个以上的方法。

一些朋友会说 Go 的接口是动态的,这是错误的。它们是静态的:一个接口类型的变量只会有一个静态类型,即使在运行时存储在接口变量里的值的具体类型有可能会改变,那个值也总是会满足这个接口。

我们需要特别注意这些,因为反射和接口紧密相关。

接口在 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 包含一对(value,type)数据,(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 的子集。

有一个重要的细节,一个接口类型的值内部保存的数据对总是(值,具体类型),而不是(值,接口类型)。接口不会持有接口值。

现在我们准备好了来认识反射。

反射的第一个定律

1. 从接口值到反射对象

基本上来说,反射就是一个用来检测存储在接口变量内部的值和类型的一种机制。在开始之前,我们需要知道在 reflect 包中有两种类型:Type 和 Value。我们可以通过两个简单的方法 reflect.TypeOf 和 reflect.ValueOf 从接口值获取这两种类型的值。

我们先从 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 方法,而不是一个接口值。我们可以先看一下 reflect.TypeOf 这个方法的签名:

1
2
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

该方法接收一个空接口类型的值,当我们调用 reflect.TypeOf(x) 的时候,x 首先被转换为一个空接口类型的值,然后再作为参数传递。 reflect.TypeOf 方法会从空接口值恢复 type 信息。

当然,reflect.ValueOf 是用来恢复对应的 value 值的。

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

输出:

value: <float64 Value>

reflect.Type 和 reflect.Value 这两种类型都有很多方法供我们操作。比如 Value 类型有一个 Type()方法用以返回 reflect.Value 的 Type。另外 Type 和 Value 都有一个方法 Kind() 用来返回一个常量(Uint,Float64,Slice 等等)来表示存储的是什么类型的值。Value 类型还有一些方法比如 Int()、Float()等,可以获取内部存储的具体值:

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())

输出:

type: float64
kind is float64: true
value: 3.4

还有一些其他的方法像 SetInt() 和 SetFloat,但是在使用他们之前我们需要先理解可修改性,我们将会在反射的第三个定律中讨论。

在反射的库中有几个点值得我们单独拎出来说一下。第一个需要说的就是,为了保证 API 的简洁,Value 类型的 setter 和 getter 方法 都使用了长度大的类型,比如 int64 用于所有的有符号整型,Value 类型的 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()) 

第二个需要说的就是,反射对象的 kind() 方法,返回的是底层类型的信息而不是静态类型。如果一个反射对象包含了一个用于自定义整型值,比如:

1
2
3
4
5
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
fmt.Println(reflect.TypeOf(x))
fmt.Println(v.Kind())

输出:

main.MyInt
int

v 调用 Kind(),返回的是 reflect.Int,即使 x 的静态类型是 MyInt.

反射的第二定律

2.从反射对象到接口值

就像物理中的反射一样,Go 中的反射也有自己的可逆机制

我们可以使用方法 Interface() 从一个 reflect.Value 恢复一个接口值,这个方法将值和类型信息打包成一个接口值并返回:

1
2
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

也就是我们可以这样:

1
2
var y MyInt = v.Interface().(MyInt)
fmt.Println(y)

反射第三定律

3. 要修改一个反射对象,要修改的值必须是可设置的

第三个定律比较容易困惑的,但是如果我们从第一个定律思考,就很容易理解了

下边的代码是不能运行的,但是值得学习一下

1
2
3
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 的一个属性,并不是所有的反射值 Values 都是可修改的。

我们可以通过 CanSet() 方法来判断 Value 是否可设置的。

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

输出:

settability of v: false

在一个不可修改的 Value 上调用 Set 会发生错误。什么是可修改性呢?

可修改性有点像可寻址性,但是更严格一些。反射对象是否持有原值决定了该反射对象是否具有可修改性。如下代码:

1
2
var x float64 = 3.4
v := reflect.ValueOf(x)

我们向 reflect.ValueOf 传递了一个 x 的 copy,所以创建的接口值持有的是 x 的 copy ,而不是 x 自己,所以如果下边的调用:

1
v.SetFloat(7.1)

可以被成功执行,那么 x 也不会被修改。修改的是 x 的 copy。这样是没有意义的。所以这样是非法的,可修改性这个属性的存在就是为了解决这个问题。

其实这跟函数的参数传递是很相似的:

1
2
f(x)  //x 在函数内部的修改不会影响 x
f(&x) //x 可被函数修改

如果我们想要通过反射修改 x,必须传递 x 的指针:

1
2
3
4
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 。我们可以通过 Elem() 方法来拿到 p 指向的那个值。并且保存到一个反射对象 v 中:

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

现在 v 是可设置的了

settability of v: true

我们可以通过 SetFloat() 函数来修改 x 的值:

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

输出:

7.1
7.1

所以记住一点,反射对象 Values 需要一个地址才能修改原值,否则会报错。普通的方法也是需要一个地址才能修改原值,但是不会报错。这些值适用于非引用类型的值(基本类型、struct 类型)。

对于结构体类型,除了传入指针,结构体的属性还必须是可导出才能有可设置性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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())
}
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

输出:

0: A int = 23
1: B string = skidoo
t is now {77 Sunset Strip}