阅读 482
GO语言基础篇(三十一)- go中的反射详解

GO语言基础篇(三十一)- go中的反射详解

这是我参与8月更文挑战的第 31 天,活动详情查看: 8月更文挑战

反射

Go语言提供了一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作,这种机制称为反射

在大多数的应用和服务中,虽然不会经常看到反射的使用,但是很多的框架都有依赖Go的反射机制来简化代码。我们知道Go 语言的语法元素很少、设计简单,所以它没有特别强的表达能力,但是 Go 语言的 reflect 包能够弥补它在语法上的一些劣势

reflect 实现了运行时的反射能力,能够让程序操作不同类型的对象。反射包中有两对非常重要的函数和类型,两个函数分别是:

  • reflect.TypeOf 能获取类型信息
  • reflect.ValueOf 能获取数据的运行时表示

两个类型是 reflect.Typereflect.Value,它们与上边两个函数是一一对应的关系

**类型 reflect.Type 是reflect包定义的一个接口。我们可以使用 reflect.TypeOf 函数获取任意变量的类型。**reflect.Type 接口中定义了一些有趣的方法,MethodByName 可以获取当前类型对应方法的引用、Implements 可以判断当前类型是否实现了某个接口。下边是reflect中Type接口

type Type interface {
    Align() int // 在内存中分配时,Align 返回此类型值的对齐(以字节为单位)
    FieldAlign() int // 当用作结构中的字段时,FieldAlign 返回此类型值的对齐(以字节为单位)
    Method(int) Method // 方法返回类型的方法集中的第 i 个方法
    MethodByName(string) (Method, bool) // MethodByName 返回类型的方法集中具有该名称的方法和指示是否找到该方法的布尔值
    NumMethod() int // NumMethod 返回使用 Method 可访问的方法数
    Name() string
    PkgPath() string
    Size() uintptr
    String() string
    Kind() Kind
    Implements(u Type) bool
    ...
}
复制代码

reflect包中 reflect.Value 的类型与 reflect.Type 不同,它被声明成了结构体。这个结构体没有对外暴露的字段,但是提供了获取或者写入数据的方法。具体如下:

type Value struct { //这些成员都是私有的,非导出的
    typ *rtype // typ保存由Value表示的值的类型
    ptr unsafe.Pointer // 指向数据的指针
    flag //标记保存有关该值的元数据
}

//内部实现的一些函数
func (v Value) Addr() Value
func (v Value) Bool() bool
func (v Value) Bytes() []byte
func (v Value) CanAddr() bool
复制代码

反射包中的所有方法基本都是围绕着 reflect.Type 和 reflect.Value 两个类型设计的。我们通过 reflect.TypeOf、reflect.ValueOf 可以将一个普通的变量转换成反射包中提供的 reflect.Type 和 reflect.Value类型,随后就可以使用反射包中的方法对它们进行复杂的操作

三大法则

运行时反射是程序在运行期间检查其自身结构的一种方式。反射带来的灵活性是一把双刃剑,反射作为一种元编程方式可以减少重复代码,但是过量的使用反射会使我们的程序逻辑变得难以理解并且运行缓慢

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

第一法则

反射的第一法则是我们能将 Go 语言的 interface{} 变量转换成反射对象。为什么是从 interface{} 变量到反射对象?当我们执行 reflect.ValueOf(1) 时,虽然看起来是获取了基本类型 int 对应的反射类型,但是由于 reflect.TypeOf、reflect.ValueOf 两个方法的入参都是 interface{} 类型,所以在方法执行的过程中发生了类型转换

因为Go 语言的函数调用都是值传递的,所以变量会在函数调用时进行类型转换。基本类型int会转换成interface{}类型,这也就是为什么第一条法则是从接口到反射对象

上面提到的 reflect.TypeOf 和 reflect.ValueOf 函数就能完成这里的转换,如果我们认为 Go 语言的类型和反射类型处于两个不同的世界,那么这两个函数就是连接这两个世界的桥梁

image.png

可以通过以下例子简单介绍它们的作用,reflect.TypeOf 获取了变量 author 的类型,reflect.ValueOf 获取了变量的值 draven。如果我们知道了一个变量的类型和值,那么就意味着我们知道了这个变量的全部信息

package main

import (
	"fmt"
	"reflect"
)

func main() {
	author := "draven"
	fmt.Println("TypeOf author:", reflect.TypeOf(author))
	fmt.Println("ValueOf author:", reflect.ValueOf(author))
}

$ go run main.go
TypeOf author: string
ValueOf author: draven
复制代码

有了变量的类型之后,我们可以通过 Method 方法获得类型实现的方法,通过 Field 获取类型包含的全部字段。对于不同的类型,我们也可以调用不同的方法获取相关信息

  • 结构体:获取字段的数量并通过下标和字段名获取字段StructField
  • 哈希表:获取哈希表的 Key 类型
  • 函数或方法:获取入参和返回值的类型
  • ...

总而言之,使用 reflect.TypeOf 和 reflect.ValueOf 能够获取 Go 语言中的变量对应的反射对象。一旦获取了反射对象,我们就能得到跟当前类型相关数据和操作,并可以使用这些运行时获取的结构执行方法

第二法则

反射的第二法则是我们可以从反射对象获取 interface{} 变量。既然能够将接口类型的变量转换成反射对象,那么一定需要其他方法将反射对象还原成接口类型的变量,reflect 中的 reflect.Value.Interface 就能完成这项工作

不过调用 reflect.Value.Interface 方法只能获得 interface{} 类型的变量,如果想要将其还原成最原始的状态还需要经过如下所示的显式类型转换

v := reflect.ValueOf(1)
v.Interface().(int)
复制代码

从反射对象到接口值的过程是从接口值到反射对象的镜面过程,两个过程都需要经历两次转换

  • 从接口值到反射对象

    • 从基本类型到接口类型的类型转换;
    • 从接口类型到反射对象的转换;
  • 从反射对象到接口值

    • 反射对象转换成接口类型;
    • 通过显式类型转换变成原始类型

image.png

当然不是所有的变量都需要类型转换这一过程。如果变量本身就是 interface{} 类型的,那么它不需要类型转换,因为类型转换这一过程一般都是隐式的,所以我不太需要关心它,只有在我们需要将反射对象转换回基本类型时才需要显式的转换操作

第三法则

Go 语言反射的最后一条法则是与值是否可以被更改有关,如果我们想要更新一个 reflect.Value,那么它持有的值一定是可以被更新的,假设我们有以下代码

func main() {
	i := 1
	v := reflect.ValueOf(i)
	v.SetInt(10)
	fmt.Println(i)
}

$ go run reflect.go
panic: reflect: reflect.Value.SetInt using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x82)
        /usr/local/go/src/reflect/value.go:260 +0x138
reflect.flag.mustBeAssignable(...)
        /usr/local/go/src/reflect/value.go:247
reflect.Value.SetInt(0x10adc40, 0x11548c8, 0x82, 0xa)
        /usr/local/go/src/reflect/value.go:1637 +0x3b
main.main()
        /Users/shulv/studySpace/GolangProject/src/go.language/ch12/reflect/reflect.go:19 +0xc5
复制代码

运行上述代码会导致程序崩溃并报出 “reflect: reflect.flag.mustBeAssignable using unaddressable value” 错误,仔细思考一下就能够发现出错的原因:由于 Go 语言的函数调用都是传值的,所以我们得到的反射对象跟最开始的变量没有任何关系,那么直接修改反射对象无法改变原始变量,程序为了防止错误就会崩溃

想要修改原变量只能使用如下的方法

func main() {
	i := 1
	v := reflect.ValueOf(&i)
	v.Elem().SetInt(10)
	fmt.Println(i)
}

$ go run reflect.go
10
复制代码
  1. 调用 reflect.ValueOf 获取变量指针
  2. 调用 reflect.Value.Elem 获取指针指向的变量
  3. 调用 reflect.Value.SetInt更新变量的值

由于 Go 语言的函数调用都是值传递的,所以我们只能只能用迂回的方式改变原变量:先获取指针对应的 reflect.Value,再通过 reflect.Value.Elem 方法得到可以被设置的变量,我们可以通过下面的代码理解这个过程:

func main() {
    i := 1
    v := &i
    *v = 10
}
复制代码

如果不能直接操作 i 变量修改其持有的值,我们就只能获取 i 变量所在地址并使用 *v 修改所在地址中存储的整数

类型和值

Go 语言的 interface{} 类型在语言内部是通过 reflect.emptyInterface 结体表示的,其中的 rtype 字段用于表示变量的类型,另一个 word 字段指向内部封装的数据

type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}
复制代码

用于获取变量类型的 reflect.TypeOf 函数将传入的变量隐式转换成 reflect.emptyInterface 类型并获取其中存储的类型信息 reflect.rtype

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

func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}
复制代码

reflect.rtype 是一个实现了 reflect.Type 接口的结构体,该结构体实现的 reflect.rtype.String 方法可以帮助我们获取当前类型的名称

func (t *rtype) String() string {
	s := t.nameOff(t.str).name()
	if t.tflag&tflagExtraStar != 0 {
		return s[1:]
	}
	return s
}
复制代码

reflect.TypeOf 的实现原理其实并不复杂,它只是将一个interface{}变量转换成了内部的reflect.emptyInterface表示,然后从中获取相应的类型信息

用于获取接口值reflect.Value的函数reflect.ValueOf实现也非常简单,在该函数中我们先调用了 reflect.escapes 保证当前值逃逸到堆上,然后通过reflect.unpackEface从接口中获取reflect.Value结构体:

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

	escapes(i)

	return unpackEface(i)
}

func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	t := e.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}
复制代码

reflect.unpackEface 会将传入的接口转换成 reflect.emptyInterface,然后将具体类型和指针包装成 reflect.Value 结构体后返回

当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换,将变量的类型和值转换成了 interface{} 并等待运行期间使用 reflect 包获取接口中存储的信息

更新变量

当我们想要更新 reflect.Value 时,就需要调用 reflect.Value.Set 更新反射对象,该方法会调用 reflect.flag.mustBeAssignable 和 reflect.flag.mustBeExported 分别检查当前反射对象是否是可以被设置的以及字段是否是对外公开的:

func (v Value) Set(x Value) {
	v.mustBeAssignable()
	x.mustBeExported()
	var target unsafe.Pointer
	if v.kind() == Interface {
		target = v.ptr
	}
	x = x.assignTo("reflect.Set", v.typ, target)
	typedmemmove(v.typ, v.ptr, x.ptr)
}
复制代码

reflect.Value.Set 会调用 reflect.Value.assignTo 并返回一个新的反射对象,这个返回的反射对象指针会直接覆盖原反射变量

func (v Value) assignTo(context string, dst *rtype, target unsafe.Pointer) Value {
	...
	switch {
	case directlyAssignable(dst, v.typ):
		...
		return Value{dst, v.ptr, fl}
	case implements(dst, v.typ):
		if v.Kind() == Interface && v.IsNil() {
			return Value{dst, nil, flag(Interface)}
		}
		x := valueInterface(v, false)
		if dst.NumMethod() == 0 {
			*(*interface{})(target) = x
		} else {
			ifaceE2I(dst, x, target)
		}
		return Value{dst, target, flagIndir | flag(Interface)}
	}
	panic(context + ": value of type " + v.typ.String() + " is not assignable to type " + dst.String())
}
复制代码

reflect.Value.assignTo 会根据当前和被设置的反射对象类型创建一个新的reflect.Value 结构体:

  • 如果两个反射对象的类型是可以被直接替换,就会直接返回目标反射对象
  • 如果当前反射对象是接口并且目标对象实现了接口,就会把目标对象简单包装成接口值

在变量更新的过程中,reflect.Value.assignTo返回的reflect.Value中的指针会覆盖当前反射对象中的指针实现变量的更新

参考

《Go程序设计语言》—-艾伦 A. A. 多诺万

《Go语言学习笔记》—-雨痕

Go 语言设计与实现-反射

文章分类
后端
文章标签