通过反射获取值信息
- 当我们将一个接口值传递给一个 reflect.ValueOf 函数调用时,此调用返回的是代表着此接口值的动态值的一个 reflect.Value 值。我们必须通过间接的途径获得一个代表一个接口值的 reflect.Value 值。
一个reflect.Value值的CanSet方法将返回此reflect.Value值代表的go值是否可以被修改(可以被赋值),如果一个go值可以被修改,则我们可以调用reflect.Value值的Set方法来修改此go值。
注意:reflect.ValueOf函数返回的reflect.Value值都是不可以被修改的。 - 反射不仅可以获取值的类型信息,还可以动态的获取或者修改变量的值,go语言中可以通过reflect.Value函数获取或者修改变量的值。
- 使用反射对象包装任意值
在go语言中,使用reflect.ValueOf()函数可以获取值的反射对象(reflect.Value),
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() 方法获得整型值。
func main() {
var a int = 1024
// 获取反射值对象
valueOfA := reflect.ValueOf(a)
// 获取interface{}类型的值,通过类型断言转换
getA := valueOfA.Interface().(int)
// 获取64位的值,强制类型转换为int类型
getA2 := int(valueOfA.Int())
fmt.Println(getA, getA2) // 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 提供的成员访问函数,可以获得结构体值的各种数据。
反射访问结构体成员值:
func main() {
// 获取结构体反射值对象
valueOfDummy := reflect.ValueOf(dummy{next: &dummy{}})
// 获取字段数量
fmt.Println(valueOfDummy.NumField())
// 获取索引为2的字段(float32字段),并输出字段类型
fmt.Println(valueOfDummy.Field(2).Type())
// 根据名字查找字段
fmt.Println(valueOfDummy.FieldByName("b").Type())
// 根据索引查找值中next字段的int字段的值
fmt.Println(valueOfDummy.FieldByIndex([]int{4, 0}).Type())
}
/* 输出结果
5
float32
string
int
*/
判断反射值的空和有效性
反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示。
反射值对象的零值和有效性判断方法
方 法 | 说 明 |
---|---|
IsNil() bool | 返回值是否为 nil。如果值类型不是通道(channel)、函数、接口、map、指针或 切片时发生 panic,类似于语言层的v== nil操作 |
IsValid() bool | 判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。 |
下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定,参考下面的代码。
反射值对象的零值和有效性判断:
func main() {
// *int的空指针
var a *int
fmt.Println(reflect.ValueOf(a).IsNil()) // true
// nil值
fmt.Println(reflect.ValueOf(a).IsValid()) // true
fmt.Println(reflect.ValueOf(nil).IsValid()) // false
// *int类型的空指针
fmt.Println(reflect.ValueOf((*int)(nil)).IsValid()) // true
fmt.Println(reflect.ValueOf((*int)(nil)).Elem().IsValid()) // 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
}
代码说明如下:
第 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()用于判断返回值是否有效。
通过反射获取结构体方法并调用
type Student struct {
Name string
Score int
}
// Run 注意:此处有一个关键,实现了值接收者会自动实现指针接受者,实现了指针接受者不会自动实现值接收者
func (s Student) Run(abc int, def string){
fmt.Println(abc, def)
fmt.Println("Run运行了...")
}
func main() {
s := Student{"马亚南", 100}
// 注意:此处有一个关键,实现了值接收者会自动实现指针接受者,实现了指针接受者不会自动实现值接收者
valueOfStudent := reflect.ValueOf(&s)
run := valueOfStudent.MethodByName("Run")
args := []reflect.Value{reflect.ValueOf(123), reflect.ValueOf("哈哈哈")}
run.Call(args)
}
输出结果:
123 哈哈哈
Run运行了...
通过反射修改变量的值
Go语言中类似 x、x.f[1] 和 *p 形式的表达式都可以表示变量,但是其它如 x + 1 和 f(2) 则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
对于 reflect.Values 也有类似的区别。有一些 reflect.Values 是可取地址的;其它一些则不可以。考虑以下的声明语句:
x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)
其中 a 对应的变量则不可取地址。因为 a 中的值仅仅是整数 2 的拷贝副本。b 中的值也同样不可取地址。c 中的值还是不可取地址,它只是一个指针 &x 的拷贝。
实际上所有通过reflect.ValueOf(x)返回的reflect.Value都是不可寻地址的,但是对于d,它是c的解引用方式生成的,指向另外一个变量,因此是可寻地址的,
我们可以通过调用reflect.ValueOf(&x).Elem()来获取任意变量x对应的可寻地址Value
我们可以通过调用reflect.CanAddr方法来判断其是否可以被寻地址
func main() {
x := 1
a := reflect.ValueOf(2)
b := reflect.ValueOf(x)
c := reflect.ValueOf(&x)
d := c.Elem()
fmt.Println(a.CanAddr()) // false
fmt.Println(b.CanAddr()) // false
fmt.Println(c.CanAddr()) // false
fmt.Println(d.CanAddr()) // false
}
每当我们通过指针间接地获取的 reflect.Value 都是可取地址的,即使开始的是一个不可取地址的 Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice 的索引表达式 e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。
以此类推,reflect.ValueOf(e).Index(i) 对于的值也是可取地址的,即使原始的 reflect.ValueOf(e) 不支持也没有关系。
使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。
2. 判定以获取元素的相关方法
使用 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 仍然修改值时会发生宕机。
在已知值的类型时,应尽量使用值对应类型的反射设置值。
- 值可修改条件之一-可被寻址
通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:
func main() {
var a int = 1
valueOfA := reflect.ValueOf(a)
valueOfA.SetInt(88)
fmt.Println(a)
}
程序运行崩溃:panic: reflect: reflect.Value.SetInt using unaddressable value
报错的意思是:SetInt 正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:
func main() {
var a int = 1
valueOfA := reflect.ValueOf(&a)
// 取出a地址的元素,然后修改a的值
valueOfA.Elem().SetInt(88)
fmt.Println(a) // 88
}
注意:当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的&操作;Elem() 方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。
5. 值可修改条件之二:被导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
func main() {
type dog struct {
legCount int
}
myDog := dog{}
valueOfDog := reflect.ValueOf(myDog)
valueOfDog.FieldByName("legCount").SetInt(55)
fmt.Println(myDog)
}
程序发生崩溃
panic: reflect: reflect.Value.SetInt using value obtained using unexported field
报错的意思是:SetInt() 使用的值来自于一个未导出的字段。
为了能修改这个值,需要将该字段导出。将 dog 中的 legCount 的成员首字母大写,导出 LegCount 让反射可以访问,修改后的代码如下:
func main() {
type dog struct {
LegCount int
}
myDog := dog{}
valueOfDog := reflect.ValueOf(myDog)
valueOfDog.FieldByName("LegCount").SetInt(55)
fmt.Println(myDog)
}
再次运行程序,发现仍然报错:
panic: reflect: reflect.Value.SetInt using unaddressable value
这个错误表示第 13 行构造的 valueOfDog 这个结构体实例不能被寻址,因此其字段也不能被修改。修改代码,取结构体的指针,再通过 reflect.Value 的 Elem() 方法取到值的反射值对象。修改后的完整代码如下:
func main() {
type dog struct {
LegCount int
}
myDog := dog{}
valueOfDog := reflect.ValueOf(&myDog)
valueOfDog.Elem().FieldByName("LegCount").SetInt(55)
fmt.Println(myDog)
}
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
取这个变量的地址或者这个变量所在的结构体已经是指针类型。
使用 reflect.ValueOf 进行值包装。
通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
使用 Value.Set 设置值。