# 7. Go语言反射
# 7.1 反射基本概念与类型信息获取
反射: 在编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制。
反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go语言提供了 reflect 包来访问程序的反射信息。
reflect提供了两种类型来进行访问接口变量的内容:
类型 | 作用 |
---|---|
reflect.ValueOf() | 获取输入参数接口中的数据的值,如果为空则返回0 <- 注意是0 |
reflect.TypeOf() | 动态获取输入参数接口中的值的类型,如果为空则返回nil <- 注意是nil |
reflect.Type
在Go语言程序中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。
相较于 Type 而言,Kind 所表示的范畴更大。类似于家用电器(Kind)和电视机(Type)之间的对应关系。或者电视机(Kind)和 42 寸彩色电视机(Type)。Type 是类型。Kind 是类别。Type 和 Kind 可能相同,也可能不同。通常基础数据类型的 Type 和 Kind 相同,自定义数据类型则不同。
Type指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。
而种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )
代码示例如下:
import ( "fmt" "reflect" ) // 声明一个空结构体 type MyData struct { } func main() { var a int //声明int类型变量a typeofA := reflect.TypeOf(a) // 获取变量a的反射类型对象 fmt.Println(typeofA.Kind(), typeofA.Name()) typeofData := reflect.TypeOf(MyData{}) // 获取结构体实例的反射类型对象 fmt.Println(typeofData.Kind(), typeofData.Name()) }
编译运行结果:
int int struct MyData
如上代码, 使用
typeofA.Kind()
来获取Kind
种类, 使用typeofA.Name()
来获取Type
类型 。获取指针指向的元素类型
通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个
*
操作,代码如下:import ( "fmt" "reflect" ) func main() { // 声明一个空结构体 type St struct { } // 创建实例 s := &St{} // 获取结构体实例的反射类型对象 typeOfs := reflect.TypeOf(s) // 显示反射类型对象的名称和种类 fmt.Printf("name:'%v' kind:'%v'\n", typeOfs.Name(), typeOfs.Kind()) // 取类型的元素 typeOfs = typeOfs.Elem() // 显示反射类型对象的名称和种类 fmt.Printf("element type: '%v', element kind: '%v'\n", typeOfs.Name(), typeOfs.Kind()) }
代码输出如下:
name:'' kind:'ptr' element type: 'St', element kind: 'struct'
reflect.Value
Go语言中,使用 reflect.ValueOf() 函数获得值的反射值对象(reflect.Value)。书写格式如下:
value := 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 将值以字符串类型返回 代码使用示例:
import ( "fmt" "reflect" ) func main() { var a int = 100 // 声明整型变量a并赋初值 valueofA := reflect.ValueOf(a) // 获取变量a的反射值对象 var av1 = valueofA.Interface().(int) // 获取interface{}类型的值, 通过类型断言转换 var av2 = valueofA.Int() // 获取64位的值, 强制类型转换为int类型 fmt.Println(av1, av2) }
代码输出如下:
100 100
# 7.2 反射与结构体
# 7.2.1 反射获取结构体成员类型
任意值通过 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) | 根据匹配函数匹配需要的字段。当值不是结构体或索引超界时发生宕机 |
代码演示反射访问结构体成员类型及信息:
import (
"fmt"
"reflect"
)
//定义一个学生结构体
type NewStudent struct {
Id int `json:"Id" id:"22"`
Name string `json:"Name" Name:"xjx"`
Sex bool `json:"Sex" Sex:"true"`
Class int `json:"Class" Class:"1"`
}
func main() {
// 创建NewStudent的实例
ins := NewStudent{Id: 1, Name: "xjx", Sex: true, Class: 1}
// 获取结构体实例的反射类型对象
typeOfins := reflect.TypeOf(ins)
for i := 0; i < typeOfins.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := typeOfins.Field(i)
// 输出成员名和tag
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
}
if sType, ok := typeOfins.FieldByName("Name"); ok {
// 从tag中取出需要的tag
fmt.Println(sType.Tag.Get("json"), sType.Tag.Get("Name"))
}
}
代码输出如下:
name: Id tag: 'json:"Id" id:"22"'
name: Name tag: 'json:"Name" Name:"xjx"'
name: Sex tag: 'json:"Sex" Sex:"true"'
name: Class tag: 'json:"Class" Class:"1"'
Name xjx
# 7.2.2 反射获取结构体成员值
反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如下表所示:
方 法 | 备 注 |
---|---|
Field(i int) Value | 根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机 |
NumField() int | 返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机 |
FieldByName(name string) Value | 根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) Value | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生宕机 |
FieldByNameFunc(match func(string) bool) Value | 根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机 |
代码演示反射访问结构体成员值:
import (
"fmt"
"reflect"
)
//定义一个学生结构体
type NewStudent struct {
Id int `json:"Id" id:"22"`
Name string `json:"Name" Name:"xjx"`
Sex bool `json:"Sex" Sex:"true"`
Class int `json:"Class" Class:"1"`
p *NewStudent
}
func main() {
// 创建NewStudent的实例
ins := NewStudent{Id: 1, Name: "xjx", Sex: true, Class: 1, p: &NewStudent{}}
// 获取结构体实例的反射类型对象
valueOfins := reflect.ValueOf(ins)
// 获取字段数量
fmt.Println("NumField:", valueOfins.NumField())
// 获取索引为2的字段(bool字段)
floatField := valueOfins.Field(2)
// 输出字段类型以及值
fmt.Println("Field Type:", floatField.Type(), "Field Value:", floatField.Bool())
// 根据名字查找字段
fmt.Println("FieldByName(\"Name\").Type", valueOfins.FieldByName("Name").Type())
// 根据索引查找值中, p字段的索引为2的字段的类型,即为Sex字段
fmt.Println("FieldByIndex([]int{4, 2}).Type()", valueOfins.FieldByIndex([]int{4, 2}).Type())
}
代码输出如下:
NumField: 5
Field Type: bool Field Value: true
FieldByName("Name").Type string
FieldByIndex([]int{2, 0}).Type() bool
# 7.3 反射其他操作
# 7.3.1 零值和空值判断
反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示:
方 法 | 说 明 |
---|---|
IsNil() bool | 返回值是否为 nil。如果值类型不是通道(channel)、函数、接口、map、指针或 切片时发生 panic,类似于语言层的v== nil 操作 |
IsValid() bool | 判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。 |
反射值对象的零值和有效性判断示例:
import (
"fmt"
"reflect"
)
// 实例化一个结构体
type S struct {
id int
}
func (this S) Hello() {
fmt.Println(this.id)
}
func main() {
//定义切片i
var i []int
fmt.Println("i IsValid():", reflect.ValueOf(i).IsValid(), "i IsNil():", reflect.ValueOf(i).IsNil())
//定义空指针
var p *int
fmt.Println("p IsValid():", reflect.ValueOf(p).IsValid(), "p IsNil():", reflect.ValueOf(p).IsNil())
// *int类型的空指针
fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())
// 实例化一个结构体
si := S{id: 1}
// 尝试从结构体中查找一个存在的字段
fmt.Println("存在的结构体成员:", reflect.ValueOf(si).FieldByName("id").IsValid())
fmt.Println("存在的结构体方法:", reflect.ValueOf(si).MethodByName("Hello").IsValid())
// 尝试从结构体中查找一个不存在的字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(si).FieldByName("xxx").IsValid())
fmt.Println("不存在的结构体方法:", reflect.ValueOf(si).MethodByName("hello2").IsValid())
// 实例化一个map
m := map[int]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}
编译运行结果:
i IsValid(): true i IsNil(): true
p IsValid(): true p IsNil(): true
(*int)(nil): false
存在的结构体成员: true
存在的结构体方法: true
不存在的结构体成员: false
不存在的结构体方法: false
不存在的键: false
# 7.3.2 通过反射修改值
使用 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 时会发生宕机 |
值可修改条件之一:可被寻址
通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:
func main() { i := 2 valueofI := reflect.ValueOf(i) valueofI.SetInt(3) }
程序运行崩溃,打印错误:
panic: reflect: reflect.Value.SetInt using unaddressable value
报错意思是:SetInt 正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是
i
的值,而不是i
的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:import ( "fmt" "reflect" ) func main() { i := 2 // 获取变量a的反射值对象(a的地址) valueofI := reflect.ValueOf(&i) //判断 valueofI.Elem()是否可被寻址 fmt.Println(valueofI.Elem().CanAddr()) //判断打印寻址地址 fmt.Println(valueofI.Elem().Addr()) // 取出a地址的元素(a的值) valueofI = valueofI.Elem() // 修改a的值为3 valueofI.SetInt(3) fmt.Println(i, valueofI.Int()) }
运行结果:
true 0xc000056058 3 3
# 提示
当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的
&
操作;Elem() 方法类似于语言层的*
操作,但并不代表这些方法与语言层操作等效。值可修改条件之一:被导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
import ( "fmt" "reflect" ) func main() { //定义结构体 type duck struct { id int } // 获取duck实例地址的反射值对象 v := reflect.ValueOf(&duck{id: 1}) // 取出duck实例地址的元素 v = v.Elem() //获取id 字段的值 vcount := v.FieldByName("id") // 尝试设置id的值(这里会发生崩溃) vcount.SetInt(5) fmt.Println(vcount.Int()) }
代码输出如下:
panic: reflect: reflect.Value.SetInt using value obtained using unexported field
错误提示id该值为被导出,为了能修改这个值,需要将该字段导出。将
duck
中的id
的成员首字母大写,导出Id
让反射可以访问,修改后的代码如下:import ( "fmt" "reflect" ) func main() { //定义结构体 type duck struct { Id int } // 获取duck实例地址的反射值对象 v := reflect.ValueOf(&duck{Id: 1}) // 取出duck实例地址的元素 v = v.Elem() //获取id 字段的值 vcount := v.FieldByName("Id") // 尝试设置id的值(这里会发生崩溃) vcount.SetInt(5) fmt.Println(vcount.Int()) }
结果输出:
5
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
取这个变量的地址或者这个变量所在的结构体已经是指针类型。
使用 reflect.ValueOf 进行值包装。
通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
使用 Value.Set 设置值。
# 7.3.3 通过反射创建实例
当已知 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
# 7.3.4 通过反射调用函数
如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
反射调用函数:
import (
"fmt"
"reflect"
)
//定义 MyData结构体
type MyData struct {
id int
name string
}
//定义设置ID函数
func (data *MyData) SetId(i int) {
data.id = i
}
//定义设置name函数
func (data *MyData) SetName(name string) {
data.name = name
}
//定义打印MyData结构体数据函数
func (data MyData) PrintData() {
fmt.Println(data.id, data.name)
}
func main() {
data := &MyData{id: 1, name: "test"}
v := reflect.ValueOf(&data)
params := []reflect.Value{reflect.ValueOf(2)}
v.Elem().MethodByName("SetId").Call(params)
params[0] = reflect.ValueOf("xjx")
v.Elem().MethodByName("SetName").Call(params)
v.Elem().MethodByName("PrintData").Call(nil)
}
运行结果:
2 xjx
# 提示
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。