Go语言中的反射

golang reflect

Posted by alovn on February 11, 2021

Golang的runtime包中类型元数据以及空接口和非空接口结构类型都是未导出的,所以reflect包中又定义了一套,这些类型定义在两个包中保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//runtime
type _type struct
type uncommonType struct
type interfacetype struct
type slicetype struct
...


// src/reflect/type.go
type rtype struct
type uncommonType struct
type interfaceType struct
type sliceType struct
...

reflect.Type

reflect 包提供 TypeOf 函数,用于获取一个变量的类型信息,它接收一个空接口类型的参数,并返回一个 reflect.Type 类型的返回值。

1
2
3
4
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

reflect.Type 是一个非空接口,提供了一系列方法可获取类型各方面的信息,例如对齐边界、方法、类型名称、包路径、是否实现指定接口、是否可比较…等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Type interface {
    // 对齐边界
    Align() int
    FieldAlign() int

    //方法
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int

    PkgPath() string //包路径
    Implements(u Type) bool //是否实现指定接口
    Comparable() bool //是否可比较

    //获取指针指向的元素类型
    Elem() Type
    ...
}

看下TypeOf函数使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
type Person struct {
    Name string
}
func (p Language) ShowName() {
    fmt.Println(p.Name)
}

func main() {
    a := Person{Name: "hello"}
    t := reflect.TypeOf(a)
    fmt.Println(t.Name(), t.NumMethod())
}

Go语言中传参都是值拷贝,reflect.TypeOf的参数是一个空接口类型,那么传参参数需要一个引用地址,这里传入的参数是a的 reflect.TypeOf在编译阶段会增加一个临时变量作为a的拷贝,然后再使用临时变量的地址。

其实所有参数为空接口类型的情况都要像这样,通过传递拷贝变量后的地址来实现传值的语义。

接下来TypeOf函数会将runtime.eface类型的参数转换为reflect.emptyInterface类型,并赋值给变量eface,这两个类型的结构是一致的,转换以后方便reflect包操作内部元素,因为*rtype类型实现了Type接口。所以接下来要做的就是把eface.typ包装称reflect.Type类型的返回值,TypeOf的任务就结束了。

reflect.Value

再来看看通过反射修改变量值是怎么回事,这就需要通过使用reflect.Value类型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/reflect/value.go
type Value struct {
    typ *rtype //存储反射变量的类型元数据指针
    ptr unsafe.Pointer //存储数据地址
    flag //位标识符,存储反射值的一些描述信息,例如是否为指针、是否为方法、是否只读等等
}

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }
    escapes(i)
    return unpackEface(i)
}

通常会使用reflect.ValueOf来拿到一个reflect.Value,注意reflect.ValueOf的参数也是一个空接口类型,所以和TypeOf参数处理方式一样。

除此之外,ValueOf函数会通过escapes显式的把参数指向的变量逃逸到堆上。

例子:

1
2
3
4
5
6
func main() {
    a := "hello"
    v := reflect.ValueOf(a)
    v.SetString("newhello")
    println(a)
}

这里项通过反射修改一个string类型的变量a的值。编译阶段会增加一个临时变量作为a的拷贝,同TypeOf不一样的是这个临时变量会被显式的逃逸到堆上,栈上只留它的地址。

接下来通过v调用SetString时,因为指向的是a的拷贝而不是a,而修改一个用户都不知道的临时变量没有任何意义,所以会发生panic:

1
panic: reflect: reflect.Value.SetString using unaddressable value

这样反射修改变量值是行不通的,若要修改成功就要反射a的指针,这样ValueOf函数参数指向的变量就是a的指针。

1
2
3
4
5
6
7
func main() {
    a := "hello"
    v := reflect.ValueOf(&a)
    v := v.Elem()
    v.SetString("newhello")
    println(a)
}

通过 Elem() 方法获取这个指针指向的元素类型。这个获取过程称为取元素,等效于对指针类型变量做了一个*操作。