小白学标准库之反射 reflect
1. 反射简介
反射是 元编程 概念下的一种形式,它在运行时操作不同类型的对象,检查对象的类型,大小等信息,对于没有源代码的包反射尤其有用。
设想一个场景,读取一个包中变量 a 的类型,并打印该类型的信息。可以通过 type/switch 判断如下:
switch t := a.(type) {
case int:
fmt.Println(t)
case float64:
fmt.Printf(t)
...
}
很快发现一个问题,通过 type switch 判段变量类型会有问题:
- 由于不知道变量的类型,所以 case 里需要写很多类型以获得匹配项。
- 如果类型是自定义结构体,case 无法提前预知该结构体,从而匹配不到自定义结构体。
这是 Go 的语法元素较少,设计简单导致没有特别强的表达能力,而通过反射 reflect 可以弥补,增强操作类型对象的表达能力。
2. 反射三大法则
反射中最重要的函数莫过于 reflect.TypeOf 和 reflect.ValueOf,它们对应的类型分别是 reflect.Type 和 reflect.Value:
// reflect.TypeOf, reflect.Type
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
// reflect.ValueOf, reflect.Value
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
escapes(i)
return unpackEface(i)
}
reflect.TypeOf 和 reflect.ValueOf 函数的作用是什么,这在标准库里就有描述,直接摘录如下:
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {...}
通过一段代码示例进一步了解 TypeOf 和 ValueOf:
p := person{"chunqiu", 1}
v := reflect.ValueOf(p)
fmt.Println(v)
t := reflect.TypeOf(p)
fmt.Println(t)
// result:
{chunqiu 1}
main.person
可以看到 TypeOf 返回反射对象的类型,ValueOf 返回反射对象的值。
为什么涉及到反射对象呢?这里实际上做了两次“转换”,第一次传入 reflect.TypeOf 和 reflect.ValueOf 时从具体类型转换为 interface{} 变量,接着从 interface{} 变量反射出反射对象。在反射机制下,逆着转换也是可以的,也就是从反射对象转换为 interface{} 变量。
进一步的介绍反射的三大法则,后续介绍皆围绕着三大法则进行:
- 从 interface{} 变量可以反射出反射对象。
- 从反射对象可以反射到 interface{} 变量。
- 修改反射对象,其值必须可设置。
2.1 反射法则一
从 interface{} 变量可以反射出反射对象。上例中代码即是这一情况。以 TypeOf 举例:
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
函数传参将外部传入的结构体类型变量转换为空接口类型变量 i,通过 unsafe.Pointer 指针转换指针到 emptyInterface 指针,最后解引用得到空接口类型的变量 eface,返回变量的类型 eface.typ。
这一过程需要介绍的有 unsafe.Pointer 指针和 emptyInterface 结构体。
2.1.1 unsafe.Pointer
顾名思义 unsafe.Pointer 指针是个不安全的指针,类似于 C 中的 void * 。它可以指向任何类型的变量,并且与 uintptr 结合可对内存数据进行直接运算,当然这种操作是很危险,也是官方不推荐的。
除了直接操作内存数据外,unsafe.Pointer 还可以强制转换类型的指针到特定类型的指针,如:
var i int = 1
var pi *int = &i
fmt.Printf("&pi: %v, pi: %v, *pi: %v\n", &pi, pi, *pi)
var pf *float64 = (*float64)(unsafe.Pointer(pi))
fmt.Printf("&pf: %v, pf: %v, *pf: %v\n", &pf, pf, *pf)
*pf = *pf * 10
fmt.Printf("f: %d\n", i)
// result
&pi: 0xc000006028, pi: 0xc000014088, *pi: 1
&pf: 0xc000006038, pf: 0xc000014088, *pf: 5e-324
f: 10
unsafe.Pointer 指针接受的是指针变量 pi 的值,也就是整型变量 i 的地址值。该指针变量(pi) 通过 unsafe.Pointer 这一座桥被转换为 *float64 类型的指针变量 pf,此时同 pi 一样,pf 也指向了变量 i 的地址值。修改 *pf 等于修改变量 i 的值。
在这里有两点要注意的是:
- 变量 pi 实际可以省略,可直接向 unsafe.Pointer 传变量 i 的地址 &i。
- 不能像 unsafe.Pointer 传递值,编译器会报错。
2.1.2 emptyInterface
emptyInterface 是 interface{} 类型的实际结构体表示,可在源码中查看 emptyInterface 的定义:
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
其中,rtype 用于表示变量的类型,word 是个 unsafe.Pointer 指针,它指向内部封装的数据。
再回到 eface := *(*emptyInterface)(unsafe.Pointer(&i))
这里。interface{} 变量被转换成内部的 emptyInterface 表示,然后从中获取相应的类型信息。
前面介绍了 TypeOf 函数获得类型信息的实现,进一步看 ValueOf 函数实现:
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
escapes(i)
return unpackEface(i)
}
ValueOf 首先调用 escapes 保证当前值逃逸到堆上,然后在 unpackEface 函数调用 *(*emptyInterface)(unsafe.Pointer(&i))
将空接口变量转换成内部的 emptyInterface 表示,再获取相应的类型信息。
对于 Printf 等函数打印也是用到了反射机制来识别传入变量的参数,类型等信息的。比如:
v := reflect.ValueOf(p)
fmt.Println(v)
// result:
{chunqiu 1}
v 是返回的 Value 类型的结构体,其结构体表示为:
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
通过 Println 打印的是反射对象的值。而不是 Value 的值表示。举个例子,如下结构体打印:
type ValueTest struct {
typ *int
word *int
flag bool
}
var x int = 1
vtest := ValueTest{&x, &x, true}
fmt.Println(vtest)
// result
{0xc000014088 0xc000014088 true}
针对不同类型结构体 Println 打印不同信息。
2.1.3 逃逸分析
针对上例说的 escapes 设置变量逃逸到堆上,有必要展开说明。详细了解可看 Go 逃逸分析。
这里对变量逃逸做一个总结,并对其中栈空间不足这种逃逸情况做个实践。
变量逃逸的四大情况:
- 指针逃逸;
- interface{} 动态类型逃逸。(前面的 escapes 即是设置 interface{} 变量逃逸到堆上),为什么这么设置看 escapes 函数注释即可明白:
// TODO: Maybe allow contents of a Value to live on the stack. // For now we make the contents always escape to the heap. It // makes life easier in a few places (see chanrecv/mapassign // comment below). escapes(i)
- 栈空间不足;
- 闭包;
2.1.4 栈空间不足
查看机器上栈允许占用的内存大小:
$ ulimit -a
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
...
验证是否超过一定大小的局部变量将逃逸到堆上:
func generate8191() {
nums := make([]int, 8191) // < 64KB
for i := 0; i < 8191; i++ { nums[i] = 0 }
}
func generate8192() {
nums := make([]int, 8192) // = 64KB
for i := 0; i < 8192; i++ { nums[i] = 0 }
}
func generate(n int) {
nums := make([]int, n) // 不确定大小
for i := 0; i < n; i++ { nums[i] = 0 }
}
func main() {
generate8191()
generate8192()
generate(1)
}
编译上述代码:
# go build -gcflags=-m main_stack.go
# command-line-arguments
./main_stack.go:4:6: can inline generate8191
./main_stack.go:9:6: can inline generate8192
./main_stack.go:15:6: can inline generate
./main_stack.go:20:6: can inline main
./main_stack.go:21:17: inlining call to generate8191
./main_stack.go:22:17: inlining call to generate8192
./main_stack.go:23:13: inlining call to generate
./main_stack.go:5:14: make([]int, 8191) does not escape
./main_stack.go:10:14: make([]int, 8192) escapes to heap
./main_stack.go:16:14: make([]int, n) escapes to heap
./main_stack.go:21:17: make([]int, 8191) does not escape
./main_stack.go:22:17: make([]int, 8192) escapes to heap
./main_stack.go:23:13: make([]int, n) escapes to heap
可以看到当切片内存超过或达到栈内存限制大小时将逃逸到堆上,对于不确定切片长度的对象也将逃逸到堆上。具体分析可见文章 Go 内存逃逸。
2.2 反射法则二
反射的第二法则是从反射对象反射回接口 interface{} 变量。反射回 interface{} 变量可进一步还原成变量最原始的状态。当然,对于原始状态是 interface{} 类型的,就不需要反射成变量最原始状态这一步了。
首先,从反射对象反射回接口 interface{} 变量:
p := person{"chunqiu", 27, true, false}
pi := reflect.ValueOf(p).Interface()
fmt.Printf("pi interface: %v, pi type: %T\n", pi, pi)
if v, ok := pi.(person); ok {
fmt.Printf("pi is the value of interface{}: %v\n", v)
}
// result
pi interface: {chunqiu 27 true false}, pi type: main.person
pi is the value of interface{}: {chunqiu 27 true false}
分开看:
reflect.ValueOf(p) 将原始状态转换为 interface{} 类型值,接着反射为反射对象。
reflect.ValueOf(p).Interface{} 将反射对象转换为 Interface{} 类型的值。
接着将 interface{} 类型的值转换为原始状态 person 结构体的值:
fmt.Printf("person struct: %v, person type: %T\n", reflect.ValueOf(p).Interface().(person), reflect.ValueOf(p).Interface().(person))
// result
person struct: {chunqiu 27 true false}, person type: main.person
虽然打印结果一样,但实际上 reflect.ValueOf(p).Interface().(person) 已经是原始状态 person 结构体的值而不是接口 interface{} 的值了,可以通过接口断言来验证这一点:
/*
if v, ok := reflect.ValueOf(p).Interface().(person).(p); ok {
fmt.Printf("reflect.ValueOf(p).Interface().(person) is a interface: %v\n", v)
}
*/
// invalid type assertion: reflect.ValueOf(p).Interface().(person).(p) (non-interface type person on left)
实际上法则二是法则一的逆过程,为什么可以这样可以通过 Interface() 函数继续分析,这里不详细展开了:
// Interface returns v's current value as an interface{}.
// It is equivalent to:
// var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
return valueInterface(v, true)
}
2.3 反射法则三
法则三是修改反射对象,其值必须可设置。举例如下:
type MyInt int
var m MyInt = 5
reflect.ValueOf(m).SetInt(43)
fmt.Println(m)
// result
panic: reflect: reflect.Value.SetInt using unaddressable value
运行出错,提示 using unaddressable value。这是因为传入 ValueOf 的是值,值会在赋值给函数参数的时候进行拷贝,所以在 ValueOf 内做的操作和传入的变量没有关系。需要传地址给 ValueOf:
reflect.ValueOf(&m).Elem().SetInt(43)
fmt.Println(m)
// result
43
与前面不同的是这里使用了 Elem 函数,Elem 函数会获取指针指向的变量,在调用 SetInt 更新变量的值。看起来复杂,其过程等价于:
m := (*int)(unsafe.Pointer(i))
*m = 43
3. 反射对象方法
通过 TypeOf 和 ValueOf 反射的对象可调用相应的反射对象方法获得反射对象信息。
方法很多,可从源码中查看所需的方法。这里主要介绍 Value 的 Kind 方法,Kind 方法在源码中大量使用,比如自定义结构体,对于源码包来说并不关心它的类型是哪种自定义类型,而是关心传入值是结构体 struct, 接口 interface,指针还是其它类型。通过 Kind 方法即可以查找相关变量的底层类型。
如 type 自定义 int 类型:
type MyInt int
var m MyInt = 5
t := reflect.ValueOf(m).Kind()
fmt.Println(t)
// result
int
Kind 返回 m 的底层类型 int。同理,对于 struct 和指针也可使用 Kind 获得变量的底层类型:
var age int = 1
var p *int = &age
fmt.Println(reflect.ValueOf(p).Kind())
n := name{age: 1}
fmt.Println(reflect.ValueOf(n).Kind())
// result
ptr
struct