Go语言中的反射

序言

计算机中的反射是程序检查它自己的结构的能力,尤其是通过类型;它是元编程的一种形式。它也是一个主要的难点。

在这篇文章中,我们试图通过阐明 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 和 Writer 类型,io.Reader 和 io.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)。这意味着 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{}

它表示空的方法集合,并且被任何值满足,因为任何值都拥有 0 个或多个方法。

有人说 Go 的接口是动态类型的,这是误导。接口是静态类型的:接口类型的变量永远有相同的静态类型,即便在运行时接口变量中存储的值可能改变类型,但是该值将始终满足接口。
因为反射与接口紧密相关,所以我们需要弄明白这些。

接口的表示

详细博文
接口类型的变量存储一个(value,type)对:被赋值给变量的具体值,和该值的类型描述。更准确地说,value 是实现了接口的底层具体数据项,type 描述该数据项的完整类型(full 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)

此赋值中的表达式是类型断言(type assertion);它断言的是 r 里面的数据项也实现了 io.Writer,因此我们可以把它赋值给 w。在赋值之后,w 将包含(tty,*os.File)对,它就是 r 中持有的(value,type)对。虽然接口变量里面的具体值可能拥有更大的方法集合,但是接口的静态类型决定可以调用接口变量的哪些方法。
继续,我们可以这样做:

var empty interface{}
empty = w

空接口值 empty 将包含相同的(value,type)对,(tty,*os.File)。空接口可以持有任何值,并包含我们可能需要的关于该值的全部信息。

(在这里,我们无需类型断言,因为通过静态分析可知 w 满足空接口。在把来自 Reader 的值赋给 Writer 的例子中,我们需要显式地使用类型断言,因为 Writer 的方法不是 Reader 的方法的子集。)

接口里面的(value,type)对的形式永远是(value,具体类型),不可能是(value,接口类型)。接口不能持有接口值。

现在我们开始讲解反射。

第一反射定律

1,从接口值到反射对象的反射(1. Reflection goes from interface value to reflection object.)

在底层,反射只是一种检查类型和存储在接口变量中的(value,type)对的机制。在开始之前,我们需要先了解 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

你可能想知道接口在哪里,因为该程序看起来是把 float64 类型的变量 x,而不是接口值,传递给了 reflect.TypeOf。但是 reflect.TypeOf 的签名包含空接口:

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

当我们调用 reflect.TypeOf(x) 时,x 首先被存储到空接口中,然后作为参数被传递给 reflect.TypeOf;之后 reflect.TypeOf “拆箱”(unpack),重获类型信息。

当然,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,可以抓取 Value 里面存储的值
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),这是第三反射定律的主题,下面会讨论。

反射库有两个特性值得单独指出。首先为了保持 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)

虽然 x 的静态类型是 MyInt,而不是 int,但是 v 的 Kind 仍然是 reflect.Int。换句话说,使用 Kind 无法区分 int 和 MyInt,但通过 Type 可以。

第二反射定律

2,从反射对象到接口值的反射(2. Reflection goes from reflection object to interface value.)

就像物理学上的反射,Go 中的反射也能产生自己的逆反射。

给定一个 reflect.Value,我们使用 Interface 方法可以重新获取接口值;事实上,该方法把类型和值信息“装”(pack)进接口的表示,然后返回结果:

// 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 包内部会把参数“拆箱”(unpack)。因此为了正确地打印 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

无需使用类型断言(type assertions)将 v.Interface() 的结果转换成 float64,空接口值里面包含具体值的类型信息,Printf 会重新获取它。

简而言之,Interface 方法是 ValueOf 函数的逆过程,除了它的结果永远属于静态类型 interface{}。

重申一遍:反射就是从接口值到反射对象,再返回来。

第三反射定律

3,如果想要修改反射对象,那么值必须是可设置的(settable)(3. 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 不可设置。可设置性(Settability)是 Value 的一个属性,并且不是所有 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 方法是错误的。那么什么是可设置性?

与可寻址(addressability)类似,可设置性是一个位(bit),但是很严格。它是一种反射对象能修改用于创建反射对象的实际存储的特性。可设置性由反射对象是否持有原始数据项决定。当我们执行

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

我们把 x 的副本传递给 reflect.ValueOf。所以作为 reflect.ValueOf 的参数的接口值是从 x 的副本创建的,而不是 x 本身。因此,下面的语句

v.SetFloat(7.1)

如果被允许成功,即使 v 看起来像是从 x 创建的,但是它不会更新 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) // 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。我们调用 Value 的 Elem 方法获取 p 指向的东西,称之为 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 掩饰所做的事情,但是它的功能和编程语言完全一样。请记住如果想要修改 Value 表示的数据项,需要给 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}

如果我们将程序需改成从 t,而不是 &t 创建 s,那么对 SetInt 和 SetString 的调用会失败,因为 t 的字段不是可设置的。

总结

反射定律如下:

  • 从接口值到反射对象的反射
  • 从反射对象到接口值的反射
  • 如果想要修改反射对象,那么值必须是可设置的

只要你理解了这些定律,即便反射仍然很微妙,但是在 Go 中使用反射会更容易。反射是一个很强大的工具,但是使用时应该多加小心,并且除非十分需要,否则应该避免使用反射。

还有许多我们没有讲解的反射 — 在通道上发送和接收,申请内存,使用切片和映射,调用方法和函数 — 但是这篇博文已经足够长了。在以后的文章中,我们将讲解上面提到的某些主题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值