GO 反射
虽然在大多数的应用和服务中并不常见,但是很多框架都依赖 Go 语言的反射机制简化代码。因为 Go 语言的语法元素很少、设计简单,所以它没有特别强的表达能力,但是 Go 语言的 reflect
包能够弥补它在语法上reflect.Type
的一些劣势。
reflect
实现了运行时的反射能力,能够让程序操作不同类型的对象。反射包中有两对非常重要的函数和类型,两个函数分别是:
-
reflect.TypeOf
能获取类型信息; -
reflect.ValueOf
能获取数据的运行时表示;
两个函数返回值类型是reflect.Type
和reflect.Value
,它们与函数是一一对应的关系:
类型
reflect.Type
是反射包定义的一个接口,我们可以使用 reflect.TypeOf
函数获取任意变量的类型,reflect.Type
接口中定义了一些有趣的方法,MethodByName
可以获取当前类型对应方法的引用、Implements
可以判断当前类型是否实现了某个接口:
type Type interface {
Align() int
FieldAlign() int
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
...
Implements(u Type) bool
...
}
反射包中 reflect.Value
的类型与 reflect.Type
不同,它被声明成了结构体。这个结构体没有对外暴露的字段,但是提供了获取或者写入数据的方法:
type Value struct {
// 包含过滤的或者未导出的字段
}
func (v Value) Addr() Value
func (v Value) Bool() bool
func (v Value) Bytes() []byte
...
反射包中的所有方法基本都是围绕着 reflect.Type
和 reflect.Value
两个类型设计的。我们通过 reflect.TypeOf
、reflect.ValueOf
可以将一个普通的变量转换成反射包中提供的 reflect.Type
和 reflect.Value
,随后就可以使用反射包中的方法对它们进行复杂的操作。
使用三大法则
运行时反射是程序在运行期间检查其自身结构的一种方式。反射带来的灵活性是一把双刃剑,反射作为一种元编程方式可以减少重复代码,但是过量的使用反射会使我们的程序逻辑变得难以理解并且运行缓慢。我们在这一节中会介绍 Go 语言反射的三大法则,其中包括:
- 从
interface{}
变量可以反射出反射对象; - 从反射对象可以获取
interface{}
变量; - 要修改反射对象,其值必须可设置;
第一法则
反射的第一法则是我们能将 Go 语言的 interface{}
变量转换成反射对象。很多读者可能会对这以法则产生困惑 — 为什么是从 interface{}
变量到反射对象?当我们执行 reflect.ValueOf(1)
时,虽然看起来是获取了基本类型 int
对应的反射类型,但是由于 reflect.TypeOf
、reflect.ValueOf
两个方法的入参都是 interface{}
类型,所以在方法执行的过程中发生了类型转换。
因为Go 语言的函数调用都是值传递的,所以变量会在函数调用时进行类型转换。基本类型 int 会转换成 interface{} 类型,这也就是为什么第一条法则是从接口到反射对象。
上面提到的 reflect.TypeOf
和 reflect.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
有了变量的类型之后,我们可以通过 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)
从反射对象到接口值的过程是从接口值到反射对象的镜面过程,两个过程都需要经历两次转换:
- 从接口值到反射对象:
- 从基本类型到接口类型的类型转换;
- 从接口类型到反射对象的转换;
- 从反射对象到接口值:
- 反射对象转换成接口类型;
- 通过显式类型转换变成原始类型;
当然不是所有的变量都需要类型转换这一过程。如果变量本身就是 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.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
运行上述代码会导致程序崩溃并报出 “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
- 调用
reflect.ValueOf
获取变量指针; - 调用
reflect.Value.Elem
获取指针指向的变量; - 调用
reflect.Value.SetInt
更新变量的值:
由于 Go 语言的函数调用都是值传递的,所以我们只能只能用迂回的方式改变原变量:先获取指针对应的 reflect.Value
,再通过 reflect.Value.Elem
方法得到可以被设置的变量,我们可以通过下面的代码理解这个过程:
func main() {
i := 1
v := &i
*v = 10
}
如果不能直接操作 i
变量修改其持有的值,我们就只能获取 i
变量所在地址并使用 *v
修改所在地址中存储的整数。
实现协议
reflect
包还为我们提供了 reflect.rtype.Implements
方法可以用于判断某些类型是否遵循特定的接口。在 Go 语言中获取结构体的反射类型 reflect.Type
还是比较容易的,但是想要获得接口类型需要通过以下方式:
reflect.TypeOf((*<interface>)(nil)).Elem()
我们通过一个例子在介绍如何判断一个类型是否实现了某个接口。假设我们需要判断如下代码中的 CustomError
是否实现了 Go 语言标准库中的 error
接口:
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)) // #=> true
fmt.Println(customError.Implements(typeOfError)) // #=> false
}
-
CustomError
类型并没有实现error
接口; -
*CustomError
指针类型实现了error
接口;
方法调用
作为一门静态语言,如果我们想要通过 reflect
包利用反射在运行期间执行方法不是一件容易的事情,下面的十几行代码就使用反射来执行 Add(0, 1)
函数:
func Add(a, b int) int { return a + b }
func main() {
v := reflect.ValueOf(Add)
if v.Kind() != reflect.Func {
return
}
t := v.Type()
argv := make([]reflect.Value, t.NumIn())
for i := range argv {
if t.In(i).Kind() != reflect.Int {
return
}
argv[i] = reflect.ValueOf(i)
}
result := v.Call(argv)
if len(result) != 1 || result[0].Kind() != reflect.Int {
return
}
fmt.Println(result[0].Int()) // #=> 1
}
- 通过
reflect.ValueOf
获取函数Add
对应的反射对象; - 调用
reflect.rtype.NumIn
获取函数的入参个数; - 多次调用
reflect.ValueOf
函数逐一设置argv
数组中的各个参数; - 调用反射对象
Add
的reflect.Value.Call
方法并传入参数列表; - 获取返回值数组、验证数组的长度以及类型并打印其中的数据;
使用反射来调用方法非常复杂,原本只需要一行代码就能完成的工作,现在需要十几行代码才能完成,但这也是在静态语言中使用动态特性需要付出的成本。
func (v Value) Call(in []Value) []Value {
v.mustBe(Func)
v.mustBeExported()
return v.call("Call", in)
}
Go
reflect.Value.Call
是运行时调用方法的入口,它通过两个 MustBe
开头的方法确定了当前反射对象的类型是函数以及可见性,随后调用 reflect.Value.call
完成方法调用,这个私有方法的执行过程会分成以下的几个部分:
- 检查输入参数以及类型的合法性;
- 将传入的
reflect.Value
参数数组设置到栈上; - 通过函数指针和输入参数调用函数;
- 从栈上获取函数的返回值;
小结
Go 语言的 reflect
包为我们提供了多种能力,包括如何使用反射来动态修改变量、判断类型是否实现了某些接口以及动态调用方法等功能
本文介绍了反射的使用方法,希望对你能有所帮助。