hjr 2019-03-18
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
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
代码说明如下:
在使用反射时,需要首先理解类型(Type)和种类(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 // 32位复数类型 Complex128 // 64位复数类型 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 ) type Student struct { Name string Age int } func main() { //定义一个Student类型的变量 var stu Student //获取结构体实例的反射类型对象 typeOfStu := reflect.TypeOf(stu) //显示反射类型对象的名称和种类 fmt.Println(typeOfStu.Name(), typeOfStu.Kind()) //获取Zero常量的反射类型对象 typeOfZero := reflect.TypeOf(Zero) //显示反射类型对象的名称和种类 fmt.Println(typeOfZero.Name(), typeOfZero.Kind()) }
代码输出如下:
Student struct Enum int
代码说明如下:
通过反射获取指针指向的元素类型:reflect.Elem()
Go 程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个*
操作,代码如下:
package main import ( "fmt" "reflect" ) type Student struct { Name string Age int } func main() { //定义一个Student类型的指针变量 var stu = &Student{Name:"kitty", Age: 20} //获取结构体实例的反射类型对象 typeOfStu := reflect.TypeOf(stu) //显示反射类型对象的名称和种类 fmt.Printf("name: '%v', kind: '%v'\n", typeOfStu.Name(), typeOfStu.Kind()) //取类型的元素 typeOfStu = typeOfStu.Elem() //显示反射类型对象的名称和种类 fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfStu.Name(), typeOfStu.Kind()) }
代码输出如下:
name: '', kind: 'ptr' element name: 'Student', 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) | 根据匹配函数匹配需要的字段。当值不是结构体或索引超界时发生宕机 |
Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。StructField 的结构如下:
type StructField struct { Name string // 字段名 PkgPath string // 字段路径 Type Type // 字段反射类型对象 Tag StructTag // 字段的结构体标签 Offset uintptr // 字段在结构体中的相对偏移 Index []int // Type.FieldByIndex中的返回的索引值 Anonymous bool // 是否为匿名字段 }
字段说明如下。
下面代码��,实例化一个结构体并遍历其结构体成员,再通过 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) } // 通过字段名, 找到字段类型信息 if catType, ok := typeOfCat.FieldByName("Type"); ok { // 从tag中取出需要的tag fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id")) } }
代码输出如下:
name: Name tag: '' name: Type tag: 'json:"type" id:"100"' type 100
代码说明如下:
反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。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 中获取原值,如下表所示。
方法名 | 说 明 |
---|---|
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 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。
package main import ( "fmt" "reflect" ) func main() { //声明整型变量a并赋初值 var a int = 1024 //获取变量a的反射值对象 valueOfA := reflect.ValueOf(a) //获取interface{}类型的值,通过类型断言转换 var getA int = valueOfA.Interface().(int) //获取64位的值,强制类型转换为int类型 var getB int = int(valueOfA.Int()) fmt.Println(getA, getB) }
代码输出如下:
1024 1024
代码说明如下:
反射值对象(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" ) //定义结构体 type Student struct { Name string Age int //嵌入字段 float32 bool next *Student } func main() { //值包装结构体 rValue := reflect.ValueOf(Student{ next: &Student{}, }) //获取字段数量 fmt.Println("NumField:", rValue.NumField()) //获取索引为2的字段(float32字段) //注:经过测试发现Field(i)的参数索引是从0开始的, //并且是按照定义的结构体的顺序来的,而不是按照字段名字的ASCii码值来的 floatField := rValue.Field(2) //输出字段类型 fmt.Println("Field:", floatField.Type()) //根据名字查找字段 fmt.Println("FieldByName(\"Age\").Type:", rValue.FieldByName("Age").Type()) //根据索引查找值中next字段的int字段的值 fmt.Println("FieldByIndex([]int{4, 0}).Type()", rValue.FieldByIndex([]int{4, 0}).Type()) }
输出结果为:
NumField: 5 Field: float32 FieldByName("Age").Type: int FieldByIndex([]int{4, 0}).Type() string
代码说明如下:
Age
字符串,查找到 Age 字段的类型。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() { //*int的空指针 var a *int fmt.Println("var a *int:", reflect.ValueOf(a).IsNil()) //nil值 fmt.Println("nil:", reflect.ValueOf(nil).IsValid()) //*int类型的空指针 fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid()) //实例化一个结构体 s := struct {}{} //尝试从结构体中查找一个不存在的字段 fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid()) //尝试从结构体中查找一个不存在的方法 fmt.Println("不存在的方法:", reflect.ValueOf(s).MethodByName("").IsValid()) //实例化一个map m := map[int]int{} //尝试从map中查找一个不存在的键 fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid()) }
输出结果:
var a *int: true nil: false (*int)(nil): false 不存在的结构体成员: false 不存在的方法: false 不存在的键: false
代码说明如下:
IsNil() 常被用于判断指针是否为空;IsValid() 常被用于判定返回值是否有效。
使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。
使用 reflect.Value 取元素、取地址及修改值的属性方法请参考下表。
方法名 | 备 注 |
---|---|
Elem() Value | 取值指向的元素值,类似于语言层* 操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value |
Addr() Value | 对可寻址的值返回其地址,类似于语言层& 操作。当值不可寻址时发生宕机 |
CanAddr() bool | 表示值是否可寻址 |
CanSet() bool | 返回值能否被修改。要求值可寻址且是导出的字段 |
使用 reflect.Value 修改值的相关方法如下表所示。
Set(x 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() { //声明整形变量a并赋初值 var a int = 1024 //获取变量a的反射值对象 rValue := reflect.ValueOf(a) //尝试将a修改为1(此处会崩溃) rValue.SetInt(1) }
程序运行崩溃,打印错误
panic: reflect: reflect.Value.SetInt using unaddressable value
报错意思是:SetInt正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:
package main import ( "fmt" "reflect" ) func main() { //声明整形变量a并赋初值 var a int = 1024 //获取变量a的反射值对象 rValue := reflect.ValueOf(&a) //取出a地址的元素(a的值) rValue = rValue.Elem() //尝试将a修改为1 rValue.SetInt(1) //打印a的值 fmt.Println(rValue.Int()) }
代码输出
1
下面是对代码的分析:
提示
当 reflect.Value 不可寻址时,使用 Addr()
方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的&
操作;Elem()
方法类似于语言层的*
操作,但并不代表这些方法与语言层操作等效。
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
package main import "reflect" func main() { type dog struct { legCount int } //获取dog实例的反射值对象 valueOfDog := reflect.ValueOf(&dog{}) valueOfDog = valueOfDog.Elem() //获取legCount字段的值 vLegCount := valueOfDog.FieldByName("legCount") //尝试设置legCount的值(这里会发生崩溃) vLegCount.SetInt(4) }
程序发生崩溃,报错:
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 } //获取dog实例的反射值对象 valueOfDog := reflect.ValueOf(&dog{}) //// 取出dog实例地址的元素 valueOfDog = valueOfDog.Elem() //获取legCount字段的值 vLegCount := valueOfDog.FieldByName("LegCount") //尝试设置legCount的值 vLegCount.SetInt(4) fmt.Println(vLegCount.Int()) }
代码输出如下:
4
代码说明如下:
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
当已知 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 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
反射调用函数:
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(2), reflect.ValueOf(3)} //反射调用函数 retList := funcValue.Call(paramList) fmt.Println(retList[0].Int()) }
代码说明如下:
提示
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。
调用方法和调用函数是一样的,只不过结构体需要先通过rValue.Method()先获取方法再调用,请看如下示例:
package main import ( "fmt" "reflect" ) type MyMath struct { Pi float64 } //普通函数 func (myMath MyMath) Sum(a, b int) int { return a + b } func (myMath MyMath) Dec(a, b int) int { return a - b } func main() { var myMath = MyMath{Pi:3.14159} //获取myMath的值对象 rValue := reflect.ValueOf(myMath) //获取到该结构体有多少个方法 //numOfMethod := rValue.NumMethod() //构造函数参数,传入两个整形值 paramList := []reflect.Value{reflect.ValueOf(30), reflect.ValueOf(20)} //调用结构体的第一个方法Method(0) //注意:在反射值对象中方法索引的顺序并不是结构体方法定义的先后顺序 //而是根据方法的ASCII码值来从小到大排序,所以Dec排在第一个,也就是Method(0) result := rValue.Method(0).Call(paramList) fmt.Println(result[0].Int()) }
代码输出结果为: