Go反射法则
反射是 Go 语言比较重要的一个特性之一,虽然在大多数的应用和服务中并不常见,但是很多框架都依赖 Go 语言的反射机制实现一些动态的功能。作为一门静态语言,Golang 在设计上都非常简洁,所以在语法上其实并没有较强的表达能力,但是 Go 语言为我们提供的 reflect
包提供的动态特性却能够弥补它在语法上的一些劣势。
reflect
实现了运行时的反射能力,能够让 Golang 的程序操作不同类型的对象,我们可以使用包中的函数 TypeOf
从静态类型 interface{}
中获取动态类型信息并通过 ValueOf
获取数据的运行时表示,通过这两个函数和包中的其他工具我们就可以得到更强大的表达能力。
概述
在 go 语言中,实现反射能力的是 reflect
包,能够让程序操作不同类型的对象。其中,在反射包中有两个非常重要的 类型和 函数,两个函数分别是:
reflect.TypeOf
- 能获取对象的类型的信息reflect.ValueOf
- 能获取对象的数据
两个类型是 reflect.Type
和 reflect.Value
,它们与函数是一一对应的关系:
反射法则
运行时反射是程序在运行期间检查其自身结构的一种方式,它是 元编程 的一种,但是它带来的灵活性也是一把双刃剑,过量的使用反射会使我们的程序逻辑变得难以理解并且运行缓慢,我们在这一节中就会介绍 Go 语言反射的三大法则,这能够帮助我们更好地理解反射的作用。
- 从接口值可反射出反射对象;
- 从反射对象可反射出接口值;
- 要修改反射对象,其值必须可设置;
第一法则
反射的第一条法则就是,我们能够将 Go 语言中的接口类型变量转换成反射对象,上面提到的reflect.TypeOf
和 reflect.ValueOf
就是完成这个转换的两个最重要方法,如果我们认为 Go 语言中的类型和反射类型是两个不同『世界』的话,那么这两个方法就是连接这两个世界的桥梁。
我们通过以下例子简单介绍这两个方法的作用,其中 TypeOf
获取了变量 author
的类型也就是 string
而 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
以及当前类型实现的接口等信息;
- 对于结构体,可以获取字段的数量并通过下标和字段名获取字段
StructField
; - 对于哈希表,可以获取哈希表的
Key
类型; - 对于函数或方法,可以获得入参和返回值的类型;
- …
总而言之,使用 TypeOf
和 ValueOf
能够将 Go 语言中的变量转换成反射对象,在这时我们能够获得几乎一切跟当前类型相关数据和操作,然后就可以用这些运行时获取的结构动态的执行一些方法。
很多读者可能都会对这个副标题产生困惑,为什么是从接口到反射对象,如果直接调用
reflect.ValueOf(1)
,看起来是从基本类型int
到反射类型,但是TypeOf
和ValueOf
两个方法的入参其实是interface{}
类型。我们在之前已经在 函数调用 一节中介绍过,Go 语言的函数调用都是值传递的,变量会在方法调用前进行类型转换,也就是
int
类型的基本变量会被转换成interface{}
类型,这也就是第一条法则介绍的是从接口到反射对象。
第二法则
我们既然能够将接口类型的变量转换成反射对象类型,那么也需要一些其他方法将反射对象还原成成接口类型的变量,reflect
中的 Interface
方法就能完成这项工作:
然而调用 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
运行上述代码时会导致程序 panic 并报出 reflect: reflect.flag.mustBeAssignable using unaddressable value
错误,仔细想一下其实能够发现出错的原因,Go 语言的 函数调用 都是传值的,所以我们得到的反射对象其实跟最开始的变量没有任何关系,没有任何变量持有复制出来的值,所以直接对它修改会导致崩溃。
想要修改原有的变量我们只能通过如下所示的方法,首先通过 reflect.ValueOf
获取变量指针,然后通过 Elem
方法获取指针指向的变量并调用 SetInt
方法更新变量的值:
func main() {
i := 1
v := reflect.ValueOf(&i)
v.Elem().SetInt(10)
fmt.Println(i)
}
$ go run reflect.go
10
这种获取指针对应的 reflect.Value
并通过 Elem
方法迂回的方式就能够获取到可以被设置的变量,这一复杂的过程主要也是因为 Go 语言的函数调用都是值传递的,我们可以将上述代码理解成:
func main() {
i := 1
v := &i
*v = 10
}
如果不能直接操作 i
变量修改其持有的值,我们就只能获取 i
变量所在地址并使用 *v
修改所在地址中存储的整树。