Go语言基础之反射
Go语言基础之反射
Go语言 reflect 包提供反射功能。
它定义了两个重要的 reflect.Type 和 reflect.Value 接口。
并且提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
大多数现代的高级语言都以各种形式支持反射功能:
- C/C++ 语言没有支持反射功能,只能通过 typeid 提供非常弱化的程序运行时类型信息;
- Java、C# 语言都支持完整的反射功能;
- Lua、JavaScript 类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。
反射是把双刃剑,通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。虽然功能强大但代码可读性并不理想,若非必要并不推荐使用反射。
Go语言使用 reflect 包来完成反射机制,提供一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
1. 反射的基本概念
反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go语言程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。
反射允许程序运行时对程序本身进行访问和修改的能力。反射主要是空接口(interface{})存储数据上,空接口(interface{})可以表达任意类型的数据,那我们如何知道这个空接口保存的数据值和数据类型了?反射就是在运行时动态的获取一个变量的类型和值。
在Go的反射定义中,任何接口都会由两部分组成的,一个是接口的具体类型,一个是具体类型对应的值。Golang中的reflect包实现了运行时反射,通过调用TypeOf函数返回一个Type类型值,该值代表运行时的数据类型,调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。
2. reflect包
在Go语言的反射机制中,任何接口值都由是一个具体类型
和具体类型的值
两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.ValueOf
两个函数来获取任意对象的Value和Type。
2.1 TypeOf
在Go语言中,使用reflect.TypeOf()
函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
该函数获得任意值的Type对象:
func TypeOf(i interface{}) Type |
Type和Kind
在反射中关于类型还划分为两种:类型(Type)
和种类(Kind)
。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)
就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
Type()、Kind()
package main |
在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。
Type
定义:types.Type,指的是系统原生数据类型,和 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。
package main |
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()
都是返回空
。
Kind
定义:reflection.Kind,指的是对象归属的种类。
type Kind uint |
Elem()
获取指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量*
操作。
package main |
NumField()、FieId~()
反射类型对象(reflect.Type)提供对结构体访问的方法:
package main |
StructField
Field()、FieldByName()、FieldByNameFunc() 等方法
返回 StructField
结构,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag
)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。
type StructField struct { |
StructTag 结构体标
reflect.StructField 结构中的 Tag 被称为结构体标签(Struct Tag),结构体标签是对结构体字段的额外信息标签。
JSON、Gorm 等进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,
格式:
`key1:"value1" key2:"value2"` |
解析与提取
根据键获取对应的值:
func (tag StructTag) Get(key string) string |
查询键是否存在:
func (tag StructTag) Lookup(key string) (value string, ok bool) |
示例:
package main |
2.2 ValueOf
reflect.ValueOf()
返回的是reflect.Value
类型,其中包含了原始值的值信息。reflect.Value
与原始值之间可以互相转换。
该函数获得值的Value对象,通常用来动态地获取或者设置变量的值。
func ValueOf(i interface{}) Value |
reflect.Value
类型提供的获取原始值的方法如下:
通过反射获取值
func reflectValue(x interface{}) { |
通过反射设置变量的值
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。
判定及获取元素的相关方法
示例:
x := 2 // value type variable? |
修改值
示例
值可修改条件:可被寻址、已经导出。
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
- 取这个变量的地址或者这个变量所在的结构体已经是指针类型。
- 使用 reflect.ValueOf 进行值包装。
- 通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
- 使用 Value.Set 设置值。
可被寻址
package main |
程序运行崩溃:panic: reflect: reflect.Value.SetInt using unaddressable value
报错的大意是:SetInt
正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:
package main |
当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的
&
操作;Elem() 方法类似于语言层的*
操作,但并不代表这些方法与语言层操作等效。
已经导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改。
package main |
程序运行崩溃:panic: reflect: reflect.Value.SetInt using value obtained using unexported field
报错的意思是:SetInt() 使用的值来自于一个未导出的字段。
isNil()和isValid()
IsNil() ;IsValid() 常被用于判定返回值是否有效。
进行零值和空判定:
isNil()
func (v Value) IsNil() bool |
IsNil()
报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
isValid()
func (v Value) IsValid() bool |
IsValid()
返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
举个例子
IsNil()
常被用于判断指针是否为空;IsValid()
常被用于判定返回值是否有效。
package main |
NumField()、Field~()
反射值对象(reflect.Value)提供对结构体访问的方法:
package main |
3. 结构体反射
json包序列化和反序列化也是使用这个实现的
3.1 与结构体相关的方法
任意值通过reflect.TypeOf()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和Field()
方法获得结构体成员的详细信息。
reflect.Type
中与获取结构体成员相关的的方法如下表所示。
3.2 StructField类型
StructField
类型用来描述结构体中的一个字段的信息。
StructField
的定义如下:
type StructField struct { |
3.3 结构体反射示例
当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。
type student struct { |
接下来编写一个函数printMethod(s interface{})
来遍历打印s包含的方法。
// 给student添加两个方法 Study和Sleep(注意首字母大写) |
4. 其他
4.1 通过类型信息创建实例
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。
例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int
,代码如下:
package main |
4.2 通过反射调用函数
- 如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。
- 使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
package main |
提示
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。
总结
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
大量使用反射的代码通常难以理解。
反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
参考感谢
Go语言基础之反射 · 语雀 (yuque.com)
03.反射介绍 · 语雀 (yuque.com)
反射 · 语雀 (yuque.com)
reflection 反射 · 语雀 (yuque.com)