写在前面

本文翻译自Go官方博客,对翻译内容有疑问,请在评论指出。

介绍

计算机的反射是程序审查自身结构的能力,特别是通过类型来反射。反射也是元编程的一种形式。我们也总是被它搞得很迷惑。

这篇文章尝试通过介绍Go中的反射是如何工作的让大家理解反射。每个语言的反射模型是不一样的(甚至有很多语言是不支持反射的),这篇文章是关于Go的,所以下文中的反射特指 “Go中的反射”。

类型和接口

因为反射是建立在类型之上的,我们首先复习一下类型。

Go是静态类型语言。每一个变量都有一个静态类型,在编译时就可以清楚的知道每个变量的类型:int, float32, *MyType, []byte 等等。如果我们定义

type MyInt int

var i int
var j MyInt

i 的类型是intj的类型是MyInt,变量i和j用明确的静态类型,尽管他们的底层真实类型是一样的(int),但是如果不通过类型转换,两个变量是不能相互指派的。

interface 类型是一个很重要的类型,它代表一组固定的方法。一个接口变量可以接受任何一个实现该接口方法的值。我们熟知的一对例子就是 io.Readerio.WriterReaderWriter类型来自io package:

// 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: 是静态类型,r的静态类型是io.Reader

一个非常重要的接口类型是空接口

interface{}

它相当于是一组空方法,能满足于任何值。因为任何值都是拥有>=0个方法的。

一些人会说Go的接口是动态类型,这是误导。一个接口类型变量总是同一个静态类型,即使在运行时,存储在接口变量中的值可能会更改类型,但该值将始终满足接口要求。

我们必须清楚的知道这些,因为反射和接口是密切相关的。

接口的表示

Russ Cox 已经写了一个关于Go接口值表示的博客detailed blog post ,没有必要再此重复说。但是下面有一个简单的总结

接口类型的变量存储一对信息:给变量分配的具体值Value和类型描述符Type,确切的说,Value是实现接口的底层具体数据项,Type描述数据项的类型。例如:

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将包含信息对 (tty, *os.File)。与r持有的信息对是一样的。接口的静态类型表明什么方法可以被此接口变量调用,尽管真实的值可能包含很多其他方法。

接下来,我们可以这样做

var empty interface{}
empty = w

空接口值empty将会被分配包含同样的信息对(tty, *os.File)。一个空接口能被赋予任意的值并且包含我们需要的所有信息。

(这里我们并不需要类型断言,因为w满足空接口要求,上一个例子中,我们将一个值从 Reader转换成 Writer,我们需要一个明确的类型断言是因为Writer的方法并不是Reader方法的子集)

一个重要的细节是接口内的信息对始终具有(值,具体类型)的形式,而不是(值,接口类型)。 接口不保存接口值。

以上介绍完毕,我们现在来看反射

反射第一原则

反射从接口值变为反射对象

在基本层面上,反射是检查一个接口变量内部存储的类型和值的机制,开始之前,需要了解这两个类型 package reflect: Type and Value。这两个类型提供访问接口变量的访问入口:两个简单的函数reflect.TypeOf and reflect.ValueOf,它们能获取接口变量对应的 reflect.Type and reflect.Value 。(通过reflect.Value可以很容易获取到reflect.Type,但是现在暂时将ValueType概念分开)

我们首先看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 reportsreflect.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.Typereflect.Value 都有很多方法让我们来操作。一个重要的例子是Value有一个Type方法返回一个reflect.ValueType。还有一个是,Type and Value都有一个叫Kind的方法,改方法返回对应存储的一个常量标示:Uint, Float64, Slice等等。Value里面还有一些像IntFloat 的方法让我们可以获取存储在其中的值(如int64 and 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

还有一些方法像:SetIntSetFloat,但是要使用它们,我们需要了解一些规则,这是下面讨论的第三反射原则的主题。

反射库有几个值得特别指出的属性。首先,为了使应用编程接口简单,Value的“getter”和“setter”方法在最大的类型上操作,该类型可以保存所有有符号整数的值:例如,int64。也就是说,ValueInt方法返回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不能将intMyInt区别开。虽然Type可以。

反射第二原则

反射从反射对象变为接口值

就像物理反射一样,Go的反射会生成自己的逆函数

利用reflect.ValueInterface方法可以恢复接口值,此方法将类型和值信息打包返回到接口值

// 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.Println(v.Interface())

(为什么不使用fmt.Println(v)?因为vreflect.Value;我们想要它拥有具体的值。)因为值是一个float64类型的,我们可以用浮点数格式打印结果:

fmt.Printf("value is %7.1e\n", v.Interface())

会得到如下结果

3.4e+00

再次声明,这里不需要通过类型断言将 v.Interface()的结果转变成float64类型;空接口值内部含有具体值的类型信息,Printf可以恢复类型信息。

简而言之,Interface()ValueOf()的逆方法,除非Interface()的结果总是静态类型interface{}

重申: 接口值<===>反射对象是一个可逆过程

反射第三原则

要修改反射对象,该值必须是可设置的

第三个原则最微妙且让人迷惑,但是我们如果是从第一个原则开始的是很容易理解的

这里有些代码不能正常运行,但是值得我们学习

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的属性,但并不是所有的反射Values都是可设置的。

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,并非x本身

v.SetFloat(7.1)

因此如果上述语句被允许成功执行,x的值并不会更新。更新的值是x的拷贝数据,这个操作是没有意义的而且会给人带来困惑。所以此操作是非法的,可设置性是用于避免此问题的属性

考虑将x传递给一个方法:

f(x)

我们不会指望f会去更新x因为我们传递的是x的值拷贝,并不是x本身。如果我们想要更新的话,我们可以将x的地址传递过去(x的指针)

f(&x)

这看起很直接且很熟悉,反射也是同样的工作方式。如果我们想要通过反射更新x,我们必须给反射库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(具体值)。我们调用ValueElem方法获取p对应的具体值。Elem方法通过指针返回一个反射Value v

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

反射可能很难理解,但是它确实在做语言所做的事情,尽管通过反射TypesValues可以掩盖发生的事情。但是 请记住,反射值需要内容的地址才能修改其表示的内容。

结构体

在我们前面的例子中,v本身不是指针,它只是从一个指针派生出来的。出现这种情况的常见方式是使用反射来修改结构的字段。只要我们有结构的地址,我们就可以修改它的字段。

这里有一个简单的例子来分析结构值t。我们用结构的地址创建反射对象,因为我们希望以后修改它。然后,我们将typeOfT设置为它的类型,并使用简单的方法调用遍历字段(请参见package 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,再调用SetIntSetString方法将会得到一个失败的结果

总结

反射原则如下:

  • 反射从接口值变为反射对象
  • 反射从反射对象变为接口值
  • 要修改反射对象,该值必须是可设置的

一旦你理解这些原则,反射将很容易使用。尽管它还是很微妙。反射是一个强大的工具,除非绝对必要,否则应该小心使用并避免使用。

还有很多关于反射的知识在这里没有涉及到——在channels中接受/发送、内存分配、使用slices和maps、调用函数和方法——但是这篇文章内容已经足够多了,我们将在后面的文章中讨论其他的主题