Go语言反射reflect标准库03-通过反射获取值信息相关操作

通过反射获取值信息

  1. 当我们将一个接口值传递给一个 reflect.ValueOf 函数调用时,此调用返回的是代表着此接口值的动态值的一个 reflect.Value 值。我们必须通过间接的途径获得一个代表一个接口值的 reflect.Value 值。
    一个reflect.Value值的CanSet方法将返回此reflect.Value值代表的go值是否可以被修改(可以被赋值),如果一个go值可以被修改,则我们可以调用reflect.Value值的Set方法来修改此go值。
    注意:reflect.ValueOf函数返回的reflect.Value值都是不可以被修改的。
  2. 反射不仅可以获取值的类型信息,还可以动态的获取或者修改变量的值,go语言中可以通过reflect.Value函数获取或者修改变量的值。
  3. 使用反射对象包装任意值
    在go语言中,使用reflect.ValueOf()函数可以获取值的反射对象(reflect.Value),
    reflect.ValueOf 返回 reflect.Value 类型,包含有 rawValue 的值信息。reflect.Value 与原值间可以通过值包装和值获取互相转化。reflect.Value 是一些反射操作的重要类型,如反射调用函数。
  4. 从反射对象获取被包装的值
    go语言中可以通过reflect.Value重新获取原始值
  5. 从反射对象(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 将值以字符串类型返回
  1. 从反射对象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 返回值能否被修改。要求值可寻址且是导出的字段
  1. 值修改相关方法
    使用 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 仍然修改值时会发生宕机。
在已知值的类型时,应尽量使用值对应类型的反射设置值。

  1. 值可修改条件之一-可被寻址
    通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:
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 设置值。

posted @ 2022-08-16 10:28  专职  阅读(484)  评论(0编辑  收藏  举报