反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go程序在运行期使用reflect包访问程序的反射信息。
reflect包实现了运行时反射,允许程序操作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。
Go 程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。
通过反射获取类型信息
通过反射获取类型信息:(reflect.TypeOf()和reflect.Type)
使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。下面通过例子来理解获取类型对象的过程:
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
}
func main() {
var stu Student
typeOfStu := reflect.TypeOf(stu)
fmt.Println(typeOfStu.Name(), typeOfStu.Kind())
}
代码输出如下:
Student struct
代码说明如下:
- 第16行,定义一个int类型的变量
- 第18行,通过reflect.TypeOf()取得变量stu的类型对象typeOfStu,类型为reflect.Type
- 第20行中,通过typeOfStu类型对象的成员函数,可以分别获取到 typeOfStu 变量的类型名为 Student,种类(Kind)为 struct。
理解反射的类型(Type)与种类(Kind)
在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如,需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
反射种类(Kind)的定义
Go 程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。
种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
type Kind uint
const (
Invalid Kind = iota
Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。
type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
从类型对象中获取类型名称和种类的例子
Go 语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串。
类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。
下面的代码中会对常量和结构体进行类型信息获取。
package main
import (
"fmt"
"reflect"
)
代码输出如下:
Student struct
Enum int
代码说明如下:
- 第21行,将 Student 实例化,并且使用 reflect.TypeOf() 获取被实例化后的 Student 的反射类型对象。
- 第27行,输出Student的类型名称和种类,类型名称就是 Student,而 Student 属于一种结构体种类,因此种类为 struct。
- 第30行,Zero 是一个 Enum 类型的常量。这个 Enum 类型在第 9 行声明,第 12 行声明了常量。如没有常量也不能创建实例,通过 reflect.TypeOf() 直接获取反射类型对象。
- 第33行,输出 Zero 对应的类型对象的类型名和种类。
reflect.Elem() - 通过反射获取指针指向的元素类型
通过反射获取指针指向的元素类型:reflect.Elem()
Go 程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个* 操作,代码如下:
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int
}
func main() {
代码输出如下:
name: '', kind: 'ptr'
element name: 'Student', element kind: 'struct'
代码说明如下:
- 第17行,创建了一个Student结构体的实例,stu是一个*Student的指针变量
- 第20行,对指针变量获取反射类型信息。
- 第23行,输出指针变量的类型名称和种类。Go语言的反射中对所有指针变量的种类都是 Ptr,但注意,指针变量的类型名称是空,不是 *Student。
- 第26行,取指针类型的元素类型,也就是 Student 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
- 第29行,输出指针变量指向元素的类型名称和种类,得到了 Student 的类型名称(Student)和种类(struct)。
通过反射获取结构体的成员类型
任意值通过 reflect.TypeOf() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 NumField() 和 Field() 方法获得结构体成员的详细信息。与成员获取相关的 reflect.Type 的方法如下表所示。
结构体成员访问的方法列表
Field(i int) StructField |
根据索引,返回索引对应的结构体字段的信息。当值不是结构体或索引超界时发生宕机 |
NumField() int |
返回结构体成员字段数量。当类型不是结构体或索引超界时发生宕机 |
FieldByName(name string) (StructField, bool) |
根据给定字符串返回字符串对应的结构体字段的信息。没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) StructField |
多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。没有找到时返回零值。当类型不是结构体或索引超界时 发生宕机 |
FieldByNameFunc( match func(string) bool) (StructField,bool) |
根据匹配函数匹配需要的字段。当值不是结构体或索引超界时发生宕机 |
结构体字段类型
Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。StructField 的结构如下:
type StructField struct {
Name string
字段说明如下。
- Name:为字段名称。
- PkgPath:字段在结构体中的路径。
- Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
- Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
- Index:FieldByIndex 中的索引顺序。
- Anonymous:表示该字段是否为匿名字段。
获取成员反射信息
下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。
反射访问结构体成员类型及信息:
package main
import (
"fmt"
"reflect"
)
func main() {
代码输出如下:
name: Name tag: ''
name: Type tag: 'json:"type" id:"100"'
type 100
代码说明如下:
- 第 11 行,声明了带有两个成员的 cat 结构体。
- 第 15 行,Type 是 cat 的一个成员,这个成员类型后面带有一个以```开始和结尾的字符串。这个字符串在 Go语言中被称为 Tag(标签)。一般用于给字段添加自定义信息,方便其他模块根据信息进行不同功能的处理。
- 第 19 行,创建 cat 实例,并对两个字段赋值。结构体标签属于类型信息,无须且不能赋值。
- 第 22 行,获取实例的反射类型对象。
- 第 25 行,使用 reflect.Type 类型的 NumField() 方法获得一个结构体类型共有多少个字段。如果类型不是结构体,将会触发宕机错误。
- 第 28 行,reflect.Type 中的 Field() 方法和 NumField 一般都是配对使用,用来实现结构体成员的遍历操作。
- 第 31 行,使用 reflect.Type 的 Field() 方法返回的结构不再是 reflect.Type 而是StructField 结构体。
- 第 35 行,使用 reflect.Type 的 FieldByName() 根据字段名查找结构体字段信息,cat Type 表示返回的结构体字段信息,类型为 StructField,ok 表示是否找到结构体字段的信息。
- 第 38 行中,使用 StructField 中 Tag 的 Get() 方法,根据 Tag 中的名字进行信息获取。
通过反射获取值信息
反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。Go语言中使用 reflect.Value 获取和设置变量的值。
变量、interface{}和reflect.Value是可以相互转换的。这点在实际开发中,会经常碰到。
使用反射值对象包装任意值
Go 语言中,使用 reflect.ValueOf() 函数获得值的反射值对象(reflect.Value)。书写格式如下:
rValue := reflect.ValueOf(rawValue)
reflect.ValueOf 返回 reflect.Value 类型,包含有 rawValue 的值信息。reflect.Value 与原值间可以通过值包装和值获取互相转化。reflect.Value 是一些反射操作的重要类型,如反射调用函数。
从反射值对象获取被包装的值
Go 语言中可以通过 reflect.Value 重新获得原始值。
从反射值对象(reflect.Value)中获取值得方法
可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,如下表所示。
反射值获取原始值的方法
Interface() interface{} |
将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 |
将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 |
将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 |
将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool |
将值以 bool 类型返回 |
Bytes() []bytes |
将值以字节数组 []bytes 类型返回 |
String() string |
将值以字符串类型返回 |
从反射值对象(reflect.Value)中获取值得例子
下面代码中,将整型变量中的值使用 reflect.Value 获取反射值对象(reflect.Value)。再通过 reflect.Value 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。
package main
import (
"fmt"
"reflect"
)
func main() {
代码输出如下:
1024 1024
代码说明如下:
- 第 11 行,声明一个变量,类型为 int,设置初值为 1024。
- 第 14 行,获取变量 a 的反射值对象,类型为 reflect.Value,这个过程和 reflect.TypeOf() 类似。
- 第 17 行,将 valueOfA 反射值对象以 interface{} 类型取出,通过类型断言转换为 int 类型并赋值给 getA。
- 第 20 行,将 valueOfA 反射值对象通过 Int 方法,以 int64 类型取出,通过强制类型转换,转换为原本的 int 类型。
通过反射访问结构体成员的值
反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如下表所示。
Field(i int) Value |
根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机 |
NumField() int |
返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机 |
FieldByName(name string) Value |
根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) Value |
多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生宕机 |
FieldByNameFunc(match func(string) bool) Value |
根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机 |
下面代码构造一个结构体包含不同类型的成员。通过 reflect.Value 提供的成员访问函数,可以获得结构体值的各种数据。
反射访问结构体成员的值:
package main
import (
"fmt"
"reflect"
)
输出结果为:
NumField: 5
Field: float32
FieldByName("Age").Type: int
FieldByIndex([]int{4, 0}).Type() string
代码说明如下:
- 第 9 行,定义结构体,结构体的每个字段的类型都不一样。
- 第 24 行,实例化结构体并包装为 reflect.Value 类型,成员中包含一个 *Student 的实例。
- 第 29 行,获取结构体的字段数量。
- 第 34 和 37 行,获取索引为2的字段值(float32 字段),并且打印类型。
- 第 39 行,根据
Age 字符串,查找到 Age 字段的类型。
- 第 41 行,[]int{4,0} 中的 4 表示,在 Student 结构中索引值为 4 的成员,也就是 next。next 的类型为 Student,也是一个结构体,因此使用 []int{4,0} 中的 0 继续在 next 值的基础上索引,结构为 Student 中索引值为 0 的 Name 字段,类型为 string。
判断反射值得空和有效性
IsNil()和IsValid() -- 判断反射值的空和有效性
反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示。
反射值对象的零值和有效性判断方法
IsNil() bool |
返回值是否为 nil。如果值类型不是通道(channel)、函数、接口、map、指针或 切片时发生 panic,类似于语言层的v== nil 操作 |
IsValid() bool |
判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。 |
下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定,参考下面的代码。
反射值对象的零值和有效性判断:
package main
import (
"fmt"
"reflect"
)
func main() {
输出结果:
var a *int: true
nil: false
(*int)(nil): false
不存在的结构体成员: false
不存在的方法: false
不存在的键: false
代码说明如下:
- 第 11 行,声明一个 *int 类型的指针,初始值为 nil。
- 第 12 行,将变量 a 包装为 reflect.Value 并且判断是否为空,此时变量 a 为空指针,因此返回 true。
- 第 15 行,对 nil 进行 IsValid() 判定(有效性判定),返回 false。
- 第 18 行,(int)(nil) 的含义是将 nil 转换为 int,也就是int 类型的空指针。此行将 nil 转换为 int 类型,并取指针指向元素。由于 nil 不指向任何元素,*int 类型的 nil 也不能指向任何元素,值不是有效的。因此这个反射值使用 Isvalid() 判断时返回 false。
- 第 21 行,实例化一个结构体。
- 第 24 行,通过 FieldByName 查找 s 结构体中一个空字符串的成员,如成员不存在,IsValid() 返回 false。
- 第 27 行,通过 MethodByName 查找 s 结构体中一个空字符串的方法,如方法不存在,IsValid() 返回 false。
- 第 30 行,实例化一个 map,这种写法与 make 方式创建的 map 等效。
- 第 33 行,MapIndex() 方法能根据给定的 reflect.Value 类型的值查找 map,并且返回查找到的结果。
IsNil() 常被用于判断指针是否为空;IsValid() 常被用于判定返回值是否有效。
通过反射修改变量的值
使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。
判断及获取元素的相关方法
使用 reflect.Value 取元素、取地址及修改值的属性方法请参考下表。
反射值对象的判定及获取元素的方法
Elem() Value |
取值指向的元素值,类似于语言层* 操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value |
Addr() Value |
对可寻址的值返回其地址,类似于语言层& 操作。当值不可寻址时发生宕机 |
CanAddr() bool |
表示值是否可寻址 |
CanSet() bool |
返回值能否被修改。要求值可寻址且是导出的字段 |
值修改相关方法
使用 reflect.Value 修改值的相关方法如下表所示。
反射值对象修改值的方法
Setlnt(x int64) |
使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机 |
SetUint(x uint64) |
使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机 |
SetFloat(x float64) |
使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机 |
SetBool(x bool) |
使用 bool 设置值。当值的类型不是 bod 时会发生宕机 |
SetBytes(x []byte) |
设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机 |
SetString(x string) |
设置字符串值。当值的类型不是 string 时会发生宕机 |
以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。
在已知值的类型时,应尽量使用值对应类型的反射设置值。
值可修改条件之一:可被寻址
通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:
package main
import "reflect"
func main() {
程序运行崩溃,打印错误
panic: reflect: reflect.Value.SetInt using unaddressable value
报错意思是:SetInt正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:
package main
import (
"fmt"
"reflect"
)
func main() {
代码输出
1
下面是对代码的分析:
- 第 14 行中,将变量 a 取值后传给 reflect.ValueOf()。此时 reflect.ValueOf() 返回的 valueOfA 持有变量 a 的地址。
- 第 17 行中,使用 reflect.Value 类型的 Elem() 方法获取 a 地址的元素,也就是 a 的值。reflect.Value 的 Elem() 方法返回的值类型也是 reflect.Value。
- 第 20 行,此时 rValue 表示的是 a 的值且可以寻址。使用 SetInt() 方法设置值时不再发生崩溃。
- 第 23 行,正确打印修改的值。
提示
当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的& 操作;Elem() 方法类似于语言层的* 操作,但并不代表这些方法与语言层操作等效。
值可修改条件之一:被导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
package main
import "reflect"
func main() {
type dog struct {
legCount int
}
程序发生崩溃,报错:
panic: reflect: reflect.Value.SetInt using value obtained using unexported field
报错的意思是:SetInt() 使用的值来自于一个未导出的字段。
为了能修改这个值,需要将该字段导出。将 dog 中的 legCount 的成员首字母大写,导出 LegCount 让反射可以访问,修改后的代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
type dog struct {
LegCount int
}
代码输出如下:
4
代码说明如下:
- 第 10 行,将 LegCount 首字母大写导出该字段。
- 第 15 行,获取 dog 实例指针的反射值对象。
- 第 19 行,取 dog 实例的指针元素,也就是 dog 的实例。
- 第 21 行,取 dog 结构体中 LegCount 字段的成员值。
- 第 24 行,修改该成员值。
- 第 26 行,打印该成员值。
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
- 取这个变量的地址或者这个变量所在的结构体已经是指针类型。
- 使用 reflect.ValueOf 进行值包装。
- 通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
- 使用 Value.SetXXX 设置值。
通过类型信息创建实例
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int ,代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int
代码输出结果如下
*int ptr
代码说明如下:
- 第 13 行,获取变量 a 的反射类型对象。
- 第 16 行,使用 reflect.New() 函数传入变量 a 的反射类型对象,创建这个类型的实例值,值以 reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。
- 第 19 行,打印 aIns 的类型为 *int,种类为指针。
通过反射调用函数
如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
反射调用函数:
package main
import (
"fmt"
"reflect"
)
代码说明如下:
- 第 9~12 行,定义一个普通的加法函数。
- 第 17 行,将 add 函数包装为反射值对象。
- 第 20 行,将 10 和 20 两个整型值使用 reflect.ValueOf 包装为 reflect.Value,再将反射值对象的切片 []reflect.Value 作为函数的参数。
- 第 23 行,使用 funcValue 函数值对象的 Call() 方法,传入参数列表 paramList 调用 add() 函数。
- 第 26 行,调用成功后,通过 retList[0] 取返回值的第一个参数,使用 Int 取返回值的整数值。
提示
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。
通过反射调用方法
调用方法和调用函数是一样的,只不过结构体需要先通过rValue.Method()先获取方法再调用,请看如下示例:
package main
import (
"fmt"
"reflect"
)
type MyMath struct {
Pi float64
}
代码输出结果为:
10
|
请发表评论