Go 语言当中的反射机制

目录

  • [Go 语言当中的反射机制](#Go 语言当中的反射机制)
  • 反射
    • [一. 三大法则](#一. 三大法则)
    • [二. 类型和值](#二. 类型和值)
    • [三. 更新变量](#三. 更新变量)
    • [四. 实现协议](#四. 实现协议)
    • [五. 方法调用](#五. 方法调用)

Go 语言当中的反射机制

由于我最近总结了 GeeRPC 项目,在 GeeRPC 的服务注册环节需要通过反射 将服务和其所具备的方法注册到服务端,因此通过这篇文章对 Golang 的反射机制 进行一次详细的学习。参考的资料是《Go 语言设计与实现》的第 4.3 节,链接如下:https://draven.co/golang/docs/part2-foundation/ch04-basic/golang-reflect/

反射

reflect 实现了运行时的反射能力,能够让程序操作不同类型的对象。

reflect 包中有两对非常重要的函数和类型。两个函数分别是:

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

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

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

Type 的定义如下:

go 复制代码
type Type interface {
       Align()int
       FieldAlign() int
       Method(int) Method
       MethodByName(string) (Method, bool)
       NumMethod() int
       ...
       Implements(u Type) bool
       ...
}

reflect.Value 类型与 Type 不同,它被声明为结构 struct。这个 struct 没有对外暴露的字段,但是提供了获取和写入数据的方法:

go 复制代码
type Value struct {
	// 包含过滤的或者未导出的字段
}

func (v Value) Addr() Value
func (v Value) Bool() bool
func (v Value) Bytes() []byte
...

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

一. 三大法则

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

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

上面引用了《Go 语言设计与实现》的原话,但是没太看懂是什么意思。下面来学习一下三大法则具体指的是什么。

第一法则

反射的第一法则是我们能将 Go 的 interface{} 变量转换为反射对象。

reflect.TypeOfreflect.ValueOf 函数能完成上述转换:

此处需要注意的是,reflect.TypeOfreflect.ValueOf 函数的形参类型是空接口类型,意味着我们可以向这两个函数中传递任意类型的变量。Golang 的函数只有值传递,意味着在函数真正执行时,传入的变量的类型已经隐式地从原有类型转为了空接口类型,因此我们是 Golag 能将空接口类型的变脸转换为反射对象,因为 reflect.TypeOfreflect.ValueOf 函数的形参类型是空接口类型。

下例介绍了 reflect.TypeOfreflect.ValueOf 函数的使用:

go 复制代码
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

不难看出,通过 reflect.TypeOf 函数能够获取传入变量的类型,而通过 reflect.ValueOf 函数可以获取当前变量保存的值。

通过变量的类型,可以通过 Method 方法获取类型实现的方法(在 GeeRPC 的服务端,就是通过这个方法来获取注册服务所具备的方法),通过 Field 获取类型包含的全部字段(即获取类型的成员)。

对于不同的类型,我们可以调用不同的方法获取相关的信息:

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

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

第二法则

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

不过调用上述方法只能将反射对象转换为 Interface{} 类型的变量,如果想要将反射对象还原成最初始的状态,还需要进行一次显式类型转换:

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

第三法则

Golang 反射机制的最后一条法则是与值是否可以被更改有关的。如果我们想要更新一个 reflect.Value,那么它持有的值一定是可以被更新的。

《Go 语言设计与实现》当中给出了这样的例子:

go 复制代码
func main() {
	i := 1
	v := reflect.ValueOf(i)
	v.SetInt(10)
	fmt.Println(i)
}

$ go run reflect.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x82, 0x1014c0)
	/usr/local/go/src/reflect/value.go:247 +0x180
reflect.flag.mustBeAssignable(...)
	/usr/local/go/src/reflect/value.go:234
reflect.Value.SetInt(0x100dc0, 0x414020, 0x82, 0x1840, 0xa, 0x0)
	/usr/local/go/src/reflect/value.go:1606 +0x40
main.main()
	/tmp/sandbox590309925/prog.go:11 +0xe0

上述代码会触发 panic,原因在于 Golang 的函数调用是传值的,所以我们得到的反射对象 v 和我们最开始的 i 没有任何关系,尝试对它进行修改,程序将会报错并崩溃。

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

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

$ go run reflect.go
10

首先我们得到反射对象 v 映射的是 i 的指针,之后通过 Elem() 方法获取对象的地址,最后通过 SetInt() 修改对象的值。

二. 类型和值

Golang 的空接口类型在语言内部是通过 reflect.emptyInterface 结构实现的。它具有两个字段,分别是 rtypeword,前者用于表示变量的类型,后者指向内部封装的数据:

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

reflect.TypeOf 函数将传入的变量隐式地转为空接口类型,即 reflect.emptyInterface 类型,并获取这个类型当中存储的 reflect.rtype

go 复制代码
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 方法可以帮助我们获取当前类型的名称:

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

总的来说,reflect.TypeOf 并不复杂,它做的就是把输入的变量转为空接口类型 reflect.emptyInterface,然后从中获取相应的类型信息。

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

go 复制代码
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.Value 时,就需要调用 reflect.Value.Set 更新反射对象。该方法会调用 reflect.flag.mustBeAssignablereflect.flag.mustBeExported 分别检查当前的反射对象是否可以被设置以及字段是否是导出的。

go 复制代码
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.rtype.Implements 方法,用于判断某些类型是否遵循特定的接口。

在 Golang 当中获取结构体的反射类型 reflect.Type 是比较简单的,直接将结构体变量传入 reflect.Type 函数即可。但是想要获得接口类型,需要通过下述方式:

go 复制代码
reflect.TypeOf((*<interface>)(nil)).Elem()

下面我们通过一个例子判断一个类型是否实现了某个接口。假定我们要判断 CustomError 是否实现 Golang 的 error 接口:

go 复制代码
type CustomError struct{}

func (*CustomError) Error() string {
	return ""
}

func main() {
	typeOfError := reflect.TypeOf((*error)(nil)).Elem()
	customErrorPtr := reflect.TypeOf(&CustomError{})
	customError := reflect.TypeOf(CustomError{})

	fmt.Println(customErrorPtr.Implements(typeOfError))
	fmt.Println(customError.Implements(typeOfError))
}
  • CustomError 类型没有实现 error 接口;
  • *CustomError 指针实现了 error 接口;

抛开执行结构不谈,我们来分析一下 reflect.rtype.Implements 的工作原理:

go 复制代码
func (t *rtype) Implements(u Type) bool {
	if u == nil {
		panic("reflect: nil type passed to Type.Implements")
	}
	if u.Kind() != Interface {
		panic("reflect: non-interface type passed to Type.Implements")
	}
	return implements(u.(*rtype), t)
}

reflect.rtype.Implements 会检查传入的类型是不是接口,如果不是接口或传入的是空值,那么会直接崩溃并终止程序。否则会调用私有函数 reflect.implements 判断类型之间是否有实现关系。

go 复制代码
func implements(T, V *rtype) bool {
	t := (*interfaceType)(unsafe.Pointer(T))
	if len(t.methods) == 0 {
		return true
	}
	...
	v := V.uncommon()
	i := 0
	vmethods := v.methods()
	for j := 0; j < int(v.mcount); j++ {
		tm := &t.methods[i]
		tmName := t.nameOff(tm.name)
		vm := vmethods[j]
		vmName := V.nameOff(vm.name)
		if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) {
			if i++; i >= len(t.methods) {
				return true
			}
		}
	}
	return false
}

如何接口不包含任何方法,那么任意类型都能自动实现该接口,返回 true

其他情况下,该方法会维护两个用于遍历接口和类型方法的所以 ij,判断是否实现了接口(逐方法检查)。

五. 方法调用

作为一门静态语言,如何我们想要通过 reflect 包利用反射在运行期间执行方法是不容易的事情,下例尝试通过反射来执行 Add 函数:

go 复制代码
func Add(a, b int) int { return a + b }

func main() {
	v := reflect.ValueOf(Add)		// 获取 Add 对应的反射对象
	if v.Kind() != reflect.Func {	// 如果值的类型不是函数, 返回
		return
	}
	t := v.Type()					// 获取函数的类型
	argv := make([]reflect.Value, t.NumIn()) // 构建一个存储 Value 的列表, NumIn 获取入参的数量
	for i := range argv {
		if t.In(i).Kind() != reflect.Int {
			return
		}
		argv[i] = reflect.ValueOf(i)	// 通过调用 ValueOf 逐一设置各个参数
	}
	result := v.Call(argv)
	if len(result) != 1 || result[0].Kind() != reflect.Int {
		return
	}
	fmt.Println(result[0].Int())
}

reflect.Value.Call 是运行时调用方法的入口,它通过两个 MustBe 开头的方法确定当前反射对象的类型是函数并确认函数的导出性。随后通过 call 完成方法调用:

go 复制代码
func (v Value) Call(in []Value) []Value {
	v.mustBe(Func)
	v.mustBeExported()
	return v.call("Call", in)
}
相关推荐
IsPrisoner14 分钟前
Go语言安装proto并且使用gRPC服务(2025最新WINDOWS系统)
开发语言·后端·golang
littleschemer15 分钟前
Go基于plugin的热更新初体验
golang·plugin·热更新
言之。1 小时前
Go语言中的函数类型参数:深入理解`func()`
golang
我重来不说话4 小时前
免费Ollama大模型集成系统——Golang
golang·gin·ollama·免费大模型
Asus.Blogs4 小时前
为什么go语言中返回的指针类型,不需要用*取值(解引用),就可以直接赋值呢?
开发语言·后端·golang
林鸿群9 小时前
go语言实现IP归属地查询
开发语言·golang·ip归属地
st紫月10 小时前
用vue和go实现登录加密
前端·vue.js·golang
YGGP11 小时前
浅析 Golang 内存管理
golang·内存泄露·内存逃逸
Chandler2412 小时前
Go 语言 net/http 包使用:HTTP 服务器、客户端与中间件
服务器·http·golang
Chandler2413 小时前
Go语言:json 作用和语法
开发语言·golang·json