Go语言之反射(二)
反射的值对象
反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。Go语言中使用reflect.Value获取和设置变量的值。
使用反射值对象包装任意值
Go语言中,使用reflect.ValueOf()函数获得值的反射值对象(reflect.Value)。书写格式如下:
value := reflect.ValueOf(rawValue)
reflect.ValueOf返回reflect.Value类型,包含有rawValue的值信息。reflect.Value与原值间可以通过值包装和值获取互相转化。reflect.Value是一些反射操作的重要类型,如反射调用函数。
从反射值对象获取被包装的值
Go 语言中可以通过 reflect.Value 重新获得原始值。
1.从反射值对象(reflect.Value)中获取值的方法
可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,如表1-2所示。
方法名 | 说明 |
Interface() interface{} | 将值以interface{}类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以int类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以uint类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以bool类型返回 |
Bytes() []bytes | 将值以字节数组[]bytes类型返回 |
String() string | 将值以字符串类型返回 |
2.从反射值对象(reflect.Value)中获取值的例子
下面代码中,将整型变量中的值使用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 getA2 int = int(valueOfA.Int()) fmt.Println(getA, getA2)
}
代码输出如下:
1024 1024
代码说明如下:
- 第11行,声明一个变量,类型为int,设置初值为1024。
- 第14行,获取变量a的反射值对象,类型为reflect.Value,这个过程和reflect.TypeOf()类似。
- 第17行,将valueOfA反射值对象以interface{}类型取出,通过类型断言转换为int类型并赋值给getA。
- 第20行,将valueOfA反射值对象通过Int方法,以int64类型取出,通过强制类型转换,转换为原本的int类型。
使用反射访问结构体成员字段的值
反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如表1-3所示。
方法 | 备注 |
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 dummy struct {
a int
b string // 嵌入字段
float32
bool next *dummy
} func main() { // 值包装结构体
d := reflect.ValueOf(dummy{
next: &dummy{},
}) // 获取字段数量
fmt.Println("NumField", d.NumField()) // 获取索引为2的字段(float32字段)
floatField := d.Field(2) // 输出字段类型
fmt.Println("Field", floatField.Type()) // 根据名字查找字段
fmt.Println("FieldByName(\"b\").Type", d.FieldByName("b").Type()) // 根据索引查找值中, next字段的int字段的值
fmt.Println("FieldByIndex([]int{4, 0}).Type()", d.FieldByIndex([]int{4, 0}).Type())
}
代码说明如下:
- 第9行,定义结构体,结构体的每个字段的类型都不一样。
- 第24行,实例化结构体并包装为reflect.Value类型,成员中包含一个*dummy的实例。
- 第29行,获取结构体的字段数量。
- 第32和35行,获取索引为2的字段值(float32字段),并且打印类型。
- 第38行,根据b字符串,查找到b字段的类型。
- 第41行,[]int{4, 0}中的4表示,在dummy结构中索引值为4的成员,也就是next。next的类型为dummy,也是一个结构体,因此使用[]int{4, 0}中的0继续在next值的基础上索引,结构为dummy中索引值为0的a字段,类型为int。
代码输出如下:
NumField 5
Field float32
FieldByName("b").Type string
FieldByIndex([]int{4, 0}).Type() int
反射对象的空和有效性判断
反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如表1-4所示。
方法 | 说明 |
IsNil() bool | 如果值类型不是通道(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("<1> var a *int:", reflect.ValueOf(a).IsNil()) // nil值
fmt.Println("<2> nil:", reflect.ValueOf(nil).IsValid()) // *int类型的空指针
fmt.Println("<3> (*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid()) // 实例化一个结构体
s := struct{}{} // 尝试从结构体中查找一个不存在的字段
fmt.Println("<4> 不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid()) // 尝试从结构体中查找一个不存在的方法
fmt.Println("<5> 不存在的结构体方法:", reflect.ValueOf(s).MethodByName("").IsValid()) // 实例化一个map
m := map[int]int{} // 尝试从map中查找一个不存在的键
fmt.Println("<6> 不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}
代码说明如下:
- 第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,并且返回查找到的结果。
代码输出如下:
<1> var a *int: true
<2> nil: false
<3> (*int)(nil): false
<4> 不存在的结构体成员: false
<5> 不存在的结构体方法: false
<6> 不存在的键: false
使用反射对象修改变量的值
使用reflect.Value对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。
1.判定及获取元素的相关方法
使用reflect.Value取元素、取地址及修改值的属性方法请参考表1-5。
方法名 | 备注 |
Elem() Value | 取值指向的元素值,类似于语言层*操作。当值类型不是指针或接口时发生宕 机,空指针时返回nil的Value |
Addr() Value | 对可寻址的值返回其地址,类似于语言层&操作。当值不可寻址时发生宕机 |
CanAddr() bool | 表示值是否可寻址 |
CanSet() bool | 返回值能否被修改。要求值可寻址且是导出的字段 |
2.值修改相关方法
使用reflect.Value修改值的相关方法如表1-6所示。
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仍然修改值时会发生宕机。在已知值的类型时,应尽量使用值对应类型的反射设置值。
3.值可修改条件之一:可被寻址
通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:
package main import (
"reflect"
) func main() { // 声明整型变量a并赋初值
var a int = 1024 // 获取变量a的反射值对象
valueOfA := reflect.ValueOf(a) // 尝试将a修改为1(此处会发生崩溃)
valueOfA.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的反射值对象(a的地址)
valueOfA := reflect.ValueOf(&a) // 取出a地址的元素(a的值)
valueOfA = valueOfA.Elem() // 修改a的值为1
valueOfA.SetInt(1) // 打印a的值
fmt.Println(valueOfA.Int())
}
代码输出如下:
1
- 第14行中,将变量a取值后传给reflect.ValueOf()。此时reflect.ValueOf()返回的valueOfA持有变量a的地址。
- 第17行中,使用reflect.Value类型的Elem()方法获取a地址的元素,也就是a的值。reflect.Value的Elem()方法返回的值类型也是reflect.Value。
- 第20行,此时valueOfA表示的是a的值且可以寻址。使用SetInt()方法设置值时不再发生崩溃。
- 第23行,正确打印修改的值。
提示:当reflect.Value不可寻址时,使用Addr()方法也是无法取到值的地址的,同时会发生宕机。虽然说reflect.Value的Addr()方法类似于语言层的&操作;Elem()方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。
4.值可修改条件之一:被导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
package main import (
"reflect"
) func main() { type dog struct {
legCount int
}
// 获取dog实例的反射值对象
valueOfDog := reflect.ValueOf(dog{}) // 获取legCount字段的值
vLegCount := valueOfDog.FieldByName("legCount") // 尝试设置legCount的值(这里会发生崩溃)
vLegCount.SetInt(4)
}
程序发生崩溃,报错:
panic: reflect: reflect.Value.SetInt using value obtained using unexported field
报错的意思是:SetInt()使用的值来自于一个未导出的字段。为了能修改这个值,需要将该字段导出。将dog中的legCount的成员首字母大写,导出LegCount让反射可以访问,修改后的代码如下:
type dog struct {
LegCount int
}
然后根据字段名获取字段的值时,将字符串的字段首字母大写,修改后的代码如下:
vLegCount := valueOfDog.FieldByName("LegCount")
再次运行程序,发现仍然报错:
panic: reflect: reflect.Value.SetInt using unaddressable value
这个错误表示第13行构造的valueOfDog这个结构体实例不能被寻址,因此其字段也不能被修改。修改代码,取结构体的指针,再通过reflect.Value的Elem()方法取到值的反射值对象。修改后的完整代码如下:
package main import (
"reflect"
"fmt"
) 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
代码说明如下:
- 第11行,将LegCount首字母大写导出该字段。
- 第14行,获取dog实例指针的反射值对象。
- 第17行,取dog实例的指针元素,也就是dog的实例。
- 第20行,取dog结构体中LegCount字段的成员值。
- 第23行,修改该成员值。
- 第25行,打印该成员值。
值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:
- 取这个变量的地址或者这个变量所在的结构体已经是指针类型。
- 使用reflect.ValueOf进行值包装。
- 通过Value.Elem()获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用set设置时会报出宕机错误。
- 使用Value.Set设置值。
通过类型创建类型的实例
当已知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
代码说明如下:
- 第13行,获取变量a的反射类型对象。
- 第16行,使用reflect.New()函数传入变量a的反射类型对象,创建这个类型的实例值,值以reflect.Value类型返回。这步操作等效于:new(int),因此返回的是*int类型的实例。
- 第19行,打印aIns的类型为*int,种类为指针。
使用反射调用函数
如果反射值对象(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(10), reflect.ValueOf(20)} // 反射调用函数
retList := funcValue.Call(paramList) // 获取第一个返回值, 取整数值
fmt.Println(retList[0].Int())
}
代码说明如下:
- 第9~12行,定义一个普通的加法函数。
- 第17行,将add函数包装为反射值对象。
- 第20行,将10和20两个整型值使用reflect.ValueOf包装为reflect.Value,再将反射值对象的切片[]reflect.Value作为函数的参数。
- 第23行,使用funcValue函数值对象的Call()方法,传入参数列表paramList调用add()函数。
- 第26行,调用成功后,通过retList[0]取返回值的第一个参数,使用Int取返回值的整数值。
提示:反射调用函数的过程需要构造大量的reflect.Value和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。
Go语言之反射(二)的更多相关文章
- Java语言基础(二) Java关键字
Java语言基础(二) Java关键字 Java关键字比较多,我就不列举出来了,只记录一些常用的小知识点: ①Java的关键字只有小写. ②then.sizeof都不是Java的关键字,熟悉C++的程 ...
- Java语言基础(二)
Java语言基础(二) 一.变量续 (1).变量有明确的类型 (2).变量必须有声明,初始化以后才能使用 (3).变量有作用域,离开作用域后自动回收 变量作用域在块内有效 (4).在同一定义域中变量不 ...
- Go语言基础(二)
Go语言基础(二) 跟着上篇,继续看Go基础 一.变量作用域 与C类似,有全局变量.局部变量.形参之分 package main import "fmt" // 全局变量 var ...
- R语言语法基础二
R语言语法基础二 重塑数据 增加行和列 # 创建向量 city = c("Tampa","Seattle","Hartford"," ...
- C语言第十二讲,文件操作.
C语言第十二讲,文件操作. 一丶文件操作概述 在操作系统中,我们的文档都称为文件.操作系统也为我们提供了接口进行操作.不同语言都是使用的相同的接口,只不过封装的上层接口不一样 操作文件的步骤 打开文件 ...
- Go语言学习笔记二: 变量
Go语言学习笔记二: 变量 今天又学了一招如何查看go的版本的命令:go version.另外上一个笔记中的代码还可以使用go run hello.go来运行,只是这种方式不会生成exe文件. 定义变 ...
- go语言通过反射获取和设置结构体字段值的方法
本文实例讲述了go语言通过反射获取和设置结构体字段值的方法.分享给大家供大家参考.具体实现方法如下: type MyStruct struct { N int } n := MyStruct{ 1 } ...
- Go语言之反射(一)
反射 反射是指在程序运行期对程序本身进行访问和修改的能力.程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分.在运行程序时,程序无法获取自身的信息.支持反射的语言可以在程序编译期将 ...
- 深度解密Go语言之反射
目录 什么是反射 为什么要用反射 反射是如何实现的 types 和 interface 反射的基本函数 反射的三大定律 反射相关函数的使用 代码样例 未导出成员 反射的实际应用 json 序列化 De ...
- Go 语言入门(二)方法和接口
写在前面 在学习 Go 语言之前,我自己是有一定的 Java 和 C++ 基础的,这篇文章主要是基于A tour of Go编写的,主要是希望记录一下自己的学习历程,加深自己的理解 Go 语言入门(二 ...
随机推荐
- Arduino连接SHT10温湿度传感器--返回值不正常解决办法
如题目,arduino中连接温湿度传感器,用的是一个github开源项目,地址:点击打开,其实这个就是一个封装好的库,下载后把解压的文件夹复制到Arduino目录下的librarys文件夹内,重启Ar ...
- 一些C/C++中的函数
项目中使用到的C/C++中的一些函数,记录下来加以理解和掌握. 1.memset( ) memset是计算机中C/C++语言函数.将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASC ...
- [转]vim 快捷键整理
Linux中vim编辑器的功能非常强大,许多常用快捷键用起来非常方便,这里将我学vim入门时学的一些常用的快捷键分享给大家一下,希望可以帮助你们. 原文地址:http://blog.csdn.net ...
- Spring 的AOP
AOP:面向切面编程,相对于OOP面向对象的编程 Spring的AOP的存在的目的是为了解耦.AOP可以让一组类共享相同的行为.在OOP中只能通过继承类和实现接口,来使代码的耦合度增强,且类继承只能为 ...
- Android商城开发系列(十三)—— 首页热卖商品布局实现
热卖商品布局效果如下图: 这个布局跟我们上节做的推荐是一样的,也是用LinearLayout和GridView去实现的,新建一个hot_item.xml,代码如下所示: <?xml versio ...
- LeetCode Valid Palindrome 有效回文(字符串)
class Solution { public: bool isPalindrome(string s) { if(s=="") return true; ) return tru ...
- Redis多机数据库
复制 PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式: ·其中完整重同步用于处理初次复制情况:完 ...
- codeforces 600A Extract Numbers
模拟题,意思是一个字符串,单词直接用','或';'来分割,可以为空,把不含前导0的整数和其他单词分别放入A和B.按照一定格式输出. 没有用stl的习惯.维护两个下标i,j,表示开区间(i,j),两段补 ...
- POJ 2385 Apple Catching(01背包)
01背包的基础上增加一个维度表示当前在的树的哪一边. #include<cstdio> #include<iostream> #include<string> #i ...
- 破解 D-H 协议
756: 破解 D-H 协议 时间限制: 1 Sec 内存限制: 128 MB提交: 78 解决: 18[提交] [状态] [讨论版] [命题人:admin] 题目描述 Diffie-Hellma ...