Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go语言程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。
reflect 包
Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
反射的类型对象(reflect.Type)
在Go语言程序中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息,下面通过示例来理解获取类型对象的过程。
下面是基本用法:
package main
import (
"fmt"
"reflect"
)
// reflect.TypeOf(),获取变量的类型,返回reflect.Type类型
// reflect.ValueOf(),获取变量的值,返回reflect.Value类型
// reflect.Value.Kind(),获取变量的类别,返回一个常量
// reflect.Value.Interface(),转换成interface{}类型
func main() {
var x float64 = 3.1415926
fmt.Println("type:", reflect.TypeOf(x)) // type: float64
v := reflect.ValueOf(x)
fmt.Println("value:", v) // value: 3.1415926
fmt.Println("type:", v.Type()) // type: float64
fmt.Println("kind:", v.Kind()) // kind: float64
fmt.Println("value:", v.Float()) // value: 3.1415926
fmt.Println(v.Interface()) // 3.1415926
fmt.Printf("value is %5.2e\n", v.Interface()) // value is 3.14e+00
y := v.Interface().(float64)
fmt.Println(y) // 3.1415926
}
反射的类型(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 // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
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"
)
// 定义一个Enum类型
type Enum int
const (
Zero Enum = 0
)
func main() {
// 声明一个空结构体
type cat struct {
}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(cat{})
// 显示反射类型对象的名称和种类
fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // cat struct.获取被实例化后的cat的反射类型对象
// 获取Zero常量的反射类型对象
typeOfA := reflect.TypeOf(Zero)
// 显示反射类型对象的名称和种类
fmt.Println(typeOfA.Name(), typeOfA.Kind()) // Enum int. 输出 Zero 对应的类型对象的类型名和种类。
}
指针与指针指向的元素
Go语言程序中对指针获取反射对象时,**可以通过 reflect.Elem() 方法获取这个指针指向的元素类型,**这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作,代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
// 声明一个空结构体
type cat struct {
}
// 创建cat的实例
ins := &cat{}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n", typeOfCat.Name(), typeOfCat.Kind()) // name:'' kind:'ptr'
// 取类型的元素
typeOfCat = typeOfCat.Elem() // 相当于取了指针对应的对象,取指针类型的元素类型,也就是 cat 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
// 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind()) // element name: 'cat', element kind: '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) :根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机
结构体字段类型
reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。
StructField 的结构如下:
type StructField struct {
Name string // 字段名
PkgPath string // 字段路径
Type Type // 字段反射类型对象
Tag StructTag // 字段的结构体标签
Offset uintptr // 字段在结构体中的相对偏移
Index []int // Type.FieldByIndex中的返回的索引值
Anonymous bool // 是否为匿名字段
}
字段说明如下:
-
Name:为字段名称。
-
PkgPath:字段在结构体中的路径。
-
Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
-
Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
-
Index:FieldByIndex 中的索引顺序。
-
Anonymous:表示该字段是否为匿名字段。
获取成员反射信息
下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。
反射访问结构体成员类型及信息:
package main
import (
"fmt"
"reflect"
)
func main() {
// 声明一个空结构体
type cat struct {
Name string
// 带有结构体tag的字段
Type int `json:"type" id:"100"`
}
// 创建cat的实例
ins := cat{Name: "mimi", Type: 1}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 遍历结构体所有成员
for i := 0; i < typeOfCat.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := typeOfCat.Field(i)
// 输出成员名和tag
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
}
// 输出结果为
// name: Name tag: ''
// name: Type tag: 'json:"type" id:"100"'
// 通过字段名, 找到字段类型信息
if catType, ok := typeOfCat.FieldByName("Type"); ok {
// 从tag中取出需要的tag
fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id")) //type 100 从配置中获得信息
}
}
结构体标签(Struct Tag)
通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(StructTag)。结构体标签是对结构体字段的额外信息标签。
JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
结构体标签的格式
key1:"value1" key2:"value2"
结构体标签由一个或多个键值对组成;键与值使用冒号分隔,值用双引号括起来;键值对之间使用一个空格分隔。
从结构体标签中获取值
StructTag 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:
-
func (tag StructTag) Get(key string) string:根据 Tag 中的键获取对应的值,例如key1:"value1" key2:"value2"的 Tag 中,可以传入“key1”获得“value1”。
-
func (tag StructTag) Lookup(key string) (value string, ok bool):根据 Tag 中的键,查询值是否存在。
结构体标签格式错误导致的问题
编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,示例代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
type cat struct {
Name string
Type int `json:"type" id:"100"` // 如果前面多一个空格都会导致查询不出来
}
typeOfCat := reflect.TypeOf(cat{})
if catType, ok := typeOfCat.FieldByName("Type"); ok {
fmt.Println(catType.Tag.Get("json"))
}
}
判断反射值的空和有效性
反射值对象(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() {
// *int的空指针
var a *int
fmt.Println("var a *int:", reflect.ValueOf(a).IsNil()) // var a *int: true
// nil值
fmt.Println("nil:", reflect.ValueOf(nil).IsValid()) // nil: false
// *int类型的空指针
fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid()) // (*int)(nil): false
// 实例化一个结构体
s := struct{}{}
// 尝试从结构体中查找一个不存在的字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid()) // 不存在的结构体成员: false
// 尝试从结构体中查找一个不存在的方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(s).MethodByName("").IsValid()) // 不存在的结构体方法: false
// 实例化一个map
m := map[int]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid()) // 不存在的键: false
}
通过类型信息创建实例
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int,代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int
// 取变量a的反射类型对象
typeOfA := reflect.TypeOf(a)
// 根据反射类型对象创建类型实例
aIns := reflect.New(typeOfA)
// 输出Value的类型和种类
fmt.Println(aIns.Type(), aIns.Kind()) // *int ptr
}
通过反射调用函数
如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
package main
import (
"fmt"
"reflect"
)
// 普通函数
func add(a, b int) int {
return a + b
}
func main() {
// 将函数包装为反射值对象
funcValue := reflect.ValueOf(add)
// 构造函数参数, 传入两个整型值
paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
// 反射调用函数
retList := funcValue.Call(paramList)
// 获取第一个返回值, 取整数值
fmt.Println(retList[0].Int()) // 30
}