译|interface 和反射的关系

原文:The Relationship Between Interfaces and Reflection


interface 是 Go 中抽象的基本工具之一。在给 interface 赋值时,interface 会存储类型信息。反射(reflection)是一种在运行时检查类型和值信息的方法。

Go 通过 reflect 包实现了反射,该包提供了检查 interface 结构元素甚至是运行时修改值的类型和方法。

在这篇文章中,我希望说明 interface 结构的各个部分与 reflect API 的关系,并最终让 reflect 包的使用更方便!

为 interface 赋值

interface 编码了三个东西:值、方法集和所存储值的类型。

interface 的结构如下所示:

interface-diagram

在该图中,我们可以清楚地看到 interface 的三个部分:_type 是类型信息,*data 是一个指向实际值的指针,而 itab 对方法集进行了编码。

当一个函数有一个 interface 参数时,将值传递给该函数会将值、方法集和类型打包到 interface 中。

利用 reflect 包,在运行时检查 interface 数据

一旦一个值被存储在 interface 中,就可以使用 reflect 包来检查其各个部分。我们无法直接检查 interface 结构;而 reflect 包维护了让我们有权访问的 interface 结构的副本。

虽然我们可以通过 reflect 对象访问 interface,但是它与底层的 interface 有直接关联。

reflect.Typereflect.Value 类型提供了访问 interface 组成部分的方法。

reflect.Type 侧重于公开与类型相关的数据,因此它仅限于 interface 结构的 _type 部分;而 reflect.Value 必须将类型信息与值相结合,以允许程序员检查和操作值,因此该类型必须查看 _type 以及 data

reflect.Type - 检查类型

reflect.TypeOf() 函数用于提取一个值的类型信息。由于它唯一的参数是一个空的 interface,因此传递给该函数的值将被赋给一个 interface,故而可以拿到类型、方法集和值。

reflect.TypeOf() 返回一个 reflect.Type,它提供了让你检查值类型的方法。

下面是一些可用的 Type 方法,以及和它们的返回相对应的 interface 结构信息。

reflect-type-diagram

reflect.Type 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"log"
"reflect"
)

type Gift struct {
Sender string
Recipient string
Number uint
Contents string
}

func main() {
g := Gift{
Sender: "Hank",
Recipient: "Sue",
Number: 1,
Contents: "Scarf",
}

t := reflect.TypeOf(g)

if kind := t.Kind(); kind != reflect.Struct {
log.Fatalf("This program expects to work on a struct; we got a %v instead.", kind)
}

for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
log.Printf("Field %03d: %-10.10s %v", i, f.Name, f.Type.Kind())
}
}

该程序的目的是打印 Gift 结构中的字段。当把 g 值传递给 reflect.TypeOf() 时,g 被赋给一个 interface,此时编译器会使用类型和方法集信息来填充它。这使得我们可以遍历该 interface 结构的类型部分的 []fields 字段,得到以下输出:

1
2
3
4
2018/12/16 12:00:00 Field 000: Sender     string
2018/12/16 12:00:00 Field 001: Recipient string
2018/12/16 12:00:00 Field 002: Number uint
2018/12/16 12:00:00 Field 003: Contents string

reflect.Method - 检查 itab / 方法集

reflect.Type 类型还允许你访问 itab 的组成部分,提取 interface 的方法信息。

reflect-method-diagram

使用 reflect 检查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"log"
"reflect"
)

type Reindeer string

func (r Reindeer) TakeOff() {
log.Printf("%q lifts off.", r)
}

func (r Reindeer) Land() {
log.Printf("%q gently lands.", r)
}

func (r Reindeer) ToggleNose() {
if r != "rudolph" {
panic("invalid reindeer operation")
}
log.Printf("%q nose changes state.", r)
}

func main() {
r := Reindeer("rudolph")

t := reflect.TypeOf(r)

for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
log.Printf("%s", m.Name)
}
}

该代码遍历了存储在 itab 中的所有函数数据,然后展示每个方法的名字:

1
2
3
2018/12/16 12:00:00 Land
2018/12/16 12:00:00 TakeOff
2018/12/16 12:00:00 ToggleNose

reflect.Value - 检查值

目前为止,我们只讨论了类型信息 —— 字段、方法等等。而 reflect.Value 为我们提供了 interface 中所存储的实际值的信息。

reflect.Value 相关联的方法必须结合类型信息和实际值。例如,为了从结构中提取字段,reflect 包必须结合结构布局信息(特别是存储在 _type 中的字段和字段偏移量),以及 interface 的 *data 部分指向的实际值,以实现正确的结构解码。

reflect-value-diagram

示例:查看修改值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package main

import (
"log"
"reflect"
)

type Child struct {
Name string
Grade int
Nice bool
}

type Adult struct {
Name string
Occupation string
Nice bool
}

// search a slice of structs for Name field that is "Hank" and set its Nice
// field to true.
func nice(i interface{}) {
// retrieve the underlying value of i. we know that i is an
// interface.
v := reflect.ValueOf(i)

// we're only interested in slices to let's check what kind of value v is. if
// it isn't a slice, return immediately.
if v.Kind() != reflect.Slice {
return
}

// v is a slice. now let's ensure that it is a slice of structs. if not,
// return immediately.
if e := v.Type().Elem(); e.Kind() != reflect.Struct {
return
}

// determine if our struct has a Name field of type string and a Nice field
// of type bool
st := v.Type().Elem()

if nameField, found := st.FieldByName("Name"); found == false || nameField.Type.Kind() != reflect.String {
return
}

if niceField, found := st.FieldByName("Nice"); found == false || niceField.Type.Kind() != reflect.Bool {
return
}

// Set any Nice fields to true where the Name is "Hank"
for i := 0; i < v.Len(); i++ {
e := v.Index(i)
name := e.FieldByName("Name")
nice := e.FieldByName("Nice")

if name.String() == "Hank" {
nice.SetBool(true)
}
}
}

func main() {
children := []Child{
{Name: "Sue", Grade: 1, Nice: true},
{Name: "Ava", Grade: 3, Nice: true},
{Name: "Hank", Grade: 6, Nice: false},
{Name: "Nancy", Grade: 5, Nice: true},
}

adults := []Adult{
{Name: "Bob", Occupation: "Carpenter", Nice: true},
{Name: "Steve", Occupation: "Clerk", Nice: true},
{Name: "Nikki", Occupation: "Rad Tech", Nice: false},
{Name: "Hank", Occupation: "Go Programmer", Nice: false},
}

log.Printf("adults before nice: %v", adults)
nice(adults)
log.Printf("adults after nice: %v", adults)

log.Printf("children before nice: %v", children)
nice(children)
log.Printf("children after nice: %v", children)
}
1
2
3
4
2018/12/16 12:00:00 adults before nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer false}]
2018/12/16 12:00:00 adults after nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer true}]
2018/12/16 12:00:00 children before nice: [{Sue 1 true} {Ava 3 true} {Hank 6 false} {Nancy 5 true}]
2018/12/16 12:00:00 children after nice: [{Sue 1 true} {Ava 3 true} {Hank 6 true} {Nancy 5 true}]

在这最后一个例子中,我们将前面学到的东西结合起来,通过 reflect.Value 来实际修改一个值。在这个场景下,有人(可能是 Hank)写了一个 nice() 函数,对于切片中的任意结构项,如果名字是“Hank”的话,它会将其从淘气(naughty)切换成友好(nice)。

注意:nice() 能够修改你传给它的任意切片的值,实际上,该函数接收到的参数类型是无关紧要的 —— 只要它是一个由结构组成的切片,并且该结构拥有 NameNice 字段。

总结

Go 中的反射是使用 interface 和 reflect 包实现的。它并没有什么神奇之处:当你使用反射时,你可以直接访问一个 interface 中的各个部分以及所存储的值。

通过这种方式,interface 几乎就像是镜像,允许程序进行自检。

虽然 Go 是一种静态类型语言,但是反射和 interface 相结合,提供了通常是动态语言独有的非常强大的技术。

有关 Go 中反射的更多信息,请务必阅读包文档,以及该主题的许多其他精彩的博客文章。