Golang: The Laws of Reflection

这只是官方文档的一篇翻译以及笔记,因为最近要使用到反射,故翻译此文。

Golang 中的反射

在计算中,反射是程序能检查自身结构的一种能力,这种检测通常通过类型实现。这是元编成的一种形式,也常常导致很多困扰。

这篇文章中,我们尝试通过解释在Go中反射是如何工作的以说明一些概念。每种语言的反射模型都不太一样(很多语言甚至不支持反射),但是这篇文章是关于Go的,所以下面内容中, “反射”一词都是指Golang中的反射。

类型和interfaces

因为反射建立在类型系统之上,我们先重新认识一下Go中的类型。

Go是静态类型的。每个变量都有一个静态类型,也就是说在编译期有唯一一个已知的、固定的类型:int, float32, *MyType, []byte, 等等。例如我们声明:

type MyInt int
var i int
var j MyInt

那么,i拥有类型 intj 的类型是 MyInt。变量 ij有不同的静态类型,尽管它们有一样的底层类型(underlying type),它们之间可以不用转换而互相赋值。

一类重要的类型是 interface,代表一组固定的方法的集合。一个 interface 类型的变量可以任何实现了这些方法的确定类型(非interface)的变量。一个被大家所知的例子是来自 io 包中的Reader和Writer类型: io.Readerio.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(or 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 类型的实例是一个空的interface:

interface{}

这代表一个空的方法集合,因此被任何值满足,因为任何值都有零个或以上的方法。

有人会说Go的interface是动态类型的,这是一种误导。它们还是静态类型的: interface类型的变量的拥有相同的静态类型,即使在运行时保存在该变量中值可能改变类型,并且这类值总需要满足interface。

我们需要明确了解这些内容,因为反射还interface是紧密相关的。

一个接口的表示/The representation of an interface

Russ Cos写了一篇关于Go中interface值的表示的 详细的博文 ,在这里没有重复这些内容,但是一个简单的总结还是必须的。

interface 类型的变量保存为一个pair:赋值给该变量的实际值,和该值的类型描述。更准确地说,值是实现该接口的底层确切的数据项,类型描述了该项的完整类型,例如:

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.Reader接口,如果是的话,就可以将它赋值为 w 。该赋值结束后, w 变量将包含 (tty, *os.File)对。这和r中持有的数据相同。 interface的静态类型决定该类型的变量可以调用哪些方法,即使其内部值可能有一个更大的方法集合。

更近一步,我们可以:

var empty interface{}
empty = w

一个空的interface值 empty再一次持有相同的 (tty, *os.File) 对。这很明了:一个空的interface能够持有任何值,并且能够包含我们所需要的关于该值的所有类型信息。

(在这里不再需要一个类型断言,因为w的静态类型就满足空interface的需要,在前面的例子中,把一个 io.Reader 赋值给一个 io.Writer 类型,需要一个明确的类型断言,是因为Writer的方法不是Reader类型的方法集合的子集)

一个重要细节是在接口内部的值-类型对的形式总是 (value, concrete type), 而不能是 (value, interface type) 这样的类型。Interfaces do not hold interface values.

下面我们开始讨论反射。

反射第一定律/The first law of reflection

1. Reflection goes from interface value to relection object.

最基础的理解,反射只是一种校验存储在interface类型内部的变量的类型和值的机制。首先,我们要知道 reflect 包中提供的两种类型: TypeValue。这两种类型提供对interface变量内部访问。reflect还提供两个简单的方法: reflect.TypeOfreflect.ValueOf, 分别从interface变量获得一个 reflect.Typereflect.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

可能你会疑惑,这里根本没有interface呀,因为这段代码传递了一个 float64 类型的变量x到reflect.TypeOf 而不是一个interface变量。但是,根据godoc的说明, reflect.TypeOf方法的签名是可以接受一个空的interface的:

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

当调用 reflect.TypeOf(x) 方法时,x首先存储在一个空的interface类型中,然后作为参数传递, reflect.TypeOf 解包一个空interface类型并获取其类型信息。

同理,reflect.ValueOf 函数从interface变量中获取值:

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 方法能返回该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

同时Value还提供 SetInt, SetFloat 之类的方法,但是要理解它们需要理解 settability,这一特性我们会在后面讨论。

reflect库有一些特点值的特别说明一下。首先,为了保证API的简单,Value的 getter 和 setter 方法基于能持有该类型的最大类型,例如对所有的singed int 类型是 int64,也就是说 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 描述底层对象的,不是静态类型,如果一个反射对象包含一个用户定一个integer类型的值,就像:

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v.Kinf() 返回的仍是 reflect.Int 类型的变量,即使x的静态类型是MyInt。换句话说, kind 方法不能区分 int 和MyInt,即使 Type 可以。

反射第二定律

Reflection goes from reflection object to interface value

像物理中的反射一样,Go中的反射也是生成自身的镜像(generates its own inverse)。

给定一个 reflect.Value 对象,我们能会使用其 Interface 方法恢复出一个interface对象;这个方法打包了类型和值信息到一个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等方法的参数都是作为空interface值传递的,然后的 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() 的返回值做类型断言,空interface类型的值拥有正确的内部值类型信息,并且会在 Printf 内部恢复它们。

重申: 反射从interface值到反射对象,再反过来(Reflection goes from interface values to reflection objects and back again).

反射第三定律

To modify a reflection object, the value must be settable.

第三条规则是最不易察觉的,也是最令人疑惑的,但是如果你是第一条规则开始的话,现在应该比较好理解了。

下面的代码并不能正确工作,但是值的学习一下:

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 不是可设置的(settable)。可设置性是 reflect.Value 的属性,不是所有的 Value类型都有。

Value类型有一个 CanSet 方法可以用来报告Value变量的可设置性,上面的代码可以改进为:

var x float64 = 3.14
v := reflect.ValueOf(x)
fmt.Println("settaility of v:", v.CanSet())

会打印 settability of v: false

settablity和可寻值性(addressability)有点类似,但更加严格。这个性质决定一个反射对象是否可以修改生成该反射对象的实际存储的对象的属性。Settability由反射对象是否持有原值(origin item)决定。当使用:

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

我们传递了x的一个拷贝到 ValueOf 调用,传递给 reflect.ValueOf 的 interface值的参数是x的一个拷贝,而不是x本身。因此, 如果表达式 v.SetFloat(7.1) 允许执行成功,它并不会真正修改 x 的值,即使看起来v是从x创建的。这样的调用是令人疑惑而且毫无意义的,所以被定义为非法的,settability这一属性就是用来避免这个问题的。

觉得这看起来奇怪?其实并不。这其实始终常见的情景的不寻常的特例(a familiar situation in unusual garb)。回想一下传递变量 x 到一个函数 f(x),我们不会期望在 f 内部能修改 x,因为我们传递的是x的值的一个拷贝,而不是x本身。如果我们需要在f内部直接修改x,我们需要传递x的地址给f(也就是x的指针): f(&x)

这看起来就很直观而且还熟悉了。反射也是这样工作的。如果我们想要通过反射修改x,我们必须传递我们想要修改的值的指针到反射库。

下面,我们尝试这样做。首先,我们像一般情况一样,初始化x,然后创建一个指向它反射值p:

var x float64 = 3.14
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() 方法,该方法通过指针间接获取值并保留结果到一个 reflect.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

反射可能时很难理解的,但是它只是做了语言所作的事情,尽管(albeit through) 通过反射 Types 和 Values 能够伪装所作的事情。只是要记住,如果需要修改其代表的对象,反射值(reflection Values)需要代表的对象的地址。

Structs

在我们前面的示例中, v 本身不是指针,而是从一个指针导出的。这种情况常用在使用反射修改一个结构体的域时。当我们有结构体的地址时,就能修改它内部的field。

下面是一个简要的示例,用于分析一个结构体变量, t。通过结构体变量的地址构造一个反射对象,因为希望在后面修改该变量。然后我们设置 typeOfT 设置为其类型,然后遍历结构体的所有域。注意我们从结构体类型获取域的名字,而域(fields)本身是一个普通的 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 的域(field)的名字时首字母大写的(exported),因为结构体中只有导出的域才能设置值。

因为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,那么调用 SetIntSetString 会失败,因为t时不可设置的。

结论

再次重申反射的三条定律:

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

一旦理解了这些规律,Go中的反射会变得容易的使用得多,尽管仍旧有点微妙。反射是一个应该被小心使用的强大工具,并且如非必要,不要使用。

还有很多反射的内容没有讲到–发射和接收channel的数据,分配内存,使用切片和map,调用函数和方法–但是这篇文章已经足够长了,我们将会在后续的文章介绍这些内容。

By Rob Pike.

原文: https://blog.golang.org/laws-of-reflection