小白学标准库之反射 reflect


1. 反射简介

反射是 元编程 概念下的一种形式,它在运行时操作不同类型的对象,检查对象的类型,大小等信息,对于没有源代码的包反射尤其有用。

设想一个场景,读取一个包中变量 a 的类型,并打印该类型的信息。可以通过 type/switch 判断如下:

switch t := a.(type) {
    case int:
        fmt.Println(t)
    case float64:
        fmt.Printf(t)
    ...
}

很快发现一个问题,通过 type switch 判段变量类型会有问题:

  1. 由于不知道变量的类型,所以 case 里需要写很多类型以获得匹配项。
  2. 如果类型是自定义结构体,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{} 变量。

进一步的介绍反射的三大法则,后续介绍皆围绕着三大法则进行:

  1. 从 interface{} 变量可以反射出反射对象。
  2. 从反射对象可以反射到 interface{} 变量。
  3. 修改反射对象,其值必须可设置。

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 的值。

在这里有两点要注意的是:

  1. 变量 pi 实际可以省略,可直接向 unsafe.Pointer 传变量 i 的地址 &i。
  2. 不能像 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 逃逸分析

这里对变量逃逸做一个总结,并对其中栈空间不足这种逃逸情况做个实践。

变量逃逸的四大情况:

  1. 指针逃逸;
  2. 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)
    
  3. 栈空间不足;
  4. 闭包;

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

posted @ 2021-11-28 10:28  hxia043  阅读(404)  评论(0编辑  收藏  举报