壹 反射介绍
根据前面学习我们可以通过fmt包和类型断言进行识别判断数据的类型和值,但是fmt包只能是直观的查看输出,并不能在程序运行时让计算机了解识别数据,而断言类型只是针对接口的,对于其他类型就爱莫能助了,这时候我们就需要引入反射,况且fmt包本质也是使用的反射,获取数据类型和值。
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
- 小插曲——变量的内在机制
- Go语言中的变量是分为两部分的:
- 类型信息:预先定义好的元信息。
- 值信息:程序运行过程中可动态变化的。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。Go程序在运行期使用reflect
包访问程序的反射信息。
空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢? 反射就是在运行时动态的获取一个变量的类型信息和值信息。反射就是在运行时动态的获取一个变量的类型信息和值信息。
贰 reflect包
在Go语言的反射机制中,任何接口值都由是一个具体类型
和具体类型的值
两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.ValueOf
两个函数来获取任意对象的Value和Type。
叁 reflect.TypeOf——查看类型
3.1 TypeOf函数介绍
在Go语言中,使用reflect.TypeOf()
函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
func main() {
var a float32 = 3.14
fmt.Printf("value:%v\ttype:%T\n", reflect.TypeOf(a), reflect.TypeOf(a)) // value:float32 type:*reflect.rtype
var b int64 = 100
fmt.Printf("value:%v\ttype:%T\n", reflect.TypeOf(b), reflect.TypeOf(b)) // value:int64 type:*reflect.rtype
}
可以看到使用reflect.TypeOf()
的值是对应的类型,其类型是*reflect.rtype
。
3.2 Name函数和Kind函数
在反射中关于类型还划分为两种:类型(Type)
和种类(Kind)
。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)
就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main
import (
"fmt"
"reflect"
)
type myInt int64
// 创建一个函数方便输出,因为空接口可以接收任意值,所以我们使用空接口做参数
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("Name函数:type=%T\tvalue=%v\t", t.Name(), t.Name())
fmt.Printf("Kind函数:type=%T\tvalue=%v\n", t.Kind(), t.Kind())
}
func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // Name函数:type=string value= Kind函数:type=reflect.Kind value=ptr
reflectType(b) // Name函数:type=string value=myInt Kind函数:type=reflect.Kind value=int64
reflectType(c) // Name函数:type=string value=int32 Kind函数:type=reflect.Kind value=int32
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "A7cc",
age: 18,
}
var e = book{title: "一只特立独行的猪"}
reflectType(d) // Name函数:type=string value=person Kind函数:type=reflect.Kind value=struct
reflectType(e) // Name函数:type=string value=book Kind函数:type=reflect.Kind value=struct
}
可以看到Name
函数返回的是字符串类型,值是当前类型,而Kind
函数返回的reflect.Kind
类型,值是底层类型。
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()
都是返回空
,个人觉得他们本质就是指针,指针本身不属于基本类型,而是一种地址类型。
在reflect
包中定义的Kind类型如下:
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 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
肆 reflect.ValueOf——查看值
4.1 ValueOf函数介绍
reflect.ValueOf()
返回的是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 |
将值以字符串类型返回 |
4.3 通过反射获取值
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
// ValueOf只能使用Kind函数获取种类,不能使用Name函数获取类型
// 其实我们可以联想下,如果可以使用Name函数,那么对于指针、切片、map,返回的就是一个空,就不能判断类型了
k := v.Kind()
//在这里我们需要使用reflect类型判断,即:reflect.Kind类型,而不是简单的int、float32等判断
switch k {
// 这里的reflect是在`reflect`包中定义的Kind类型
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type:%T, value:%v\n", v.Int(), v.Int())
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type:%T, value:%v\n", v.Float(), v.Float())
case reflect.Map:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type:%T, value:%v\n", v.Float(), v.Float())
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type:float32, value:3.140000
reflectValue(b) // type:int64, value:100
// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
// 需要区别这里的类型是返回直接使用了reflect.ValueOf的类型,而不是上面reflectValue函数使用了reflect.ValueOf再获取原始值的类型
fmt.Printf("关于reflect.ValueOf类型:type=%T, value=%v\n", c, c) // 关于reflect.ValueOf类型:type=reflect.Value, value=10
}
使用reflect.ValueOf
函数返回的值的类型是reflect.Value
,而使用reflect.Value
类型方法返回的类型是对应的基础类型。
4.4 通过反射设置变量的值——Elem函数
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()
方法来获取指针对应的值,使用这个函数需要的是reflect.ValueOf
类型。
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本,reflect包会引发panic,个人理解是SetInt内部实现需要的是一个指针,不是一个确切的值
}
}
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}
4.5 isNil函数和isValid函数
在使用这两个函数时都需要是reflect.ValueOf
类型。
4.5.1 isNil函数
func (v Value) IsNil() bool
IsNil()
是判断v这个变量持有的值是不是为nil,而不是0,一般用来判断一个指针、切片等是否初始化,所以v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。IsNil()
常被用于判断指针是否为空。
// 例子
func main() {
// *int类型空指针
var a *int
fmt.Println("a指针的值是否是为空:", reflect.ValueOf(a).IsNil()) // a指针的值是否是为空: true
// 不能为基础类型,同时可以看到使用反射时,再不运行的情况下,不会轻易报错,当运行才会报错
// var b int
// fmt.Println("b指针的值是否是为空:", reflect.ValueOf(b).IsNil())
}
4.5.2 isValid函数
func (v Value) IsValid() bool
IsValid()
是判断v是否持有一个值,与isNil
函数不同,同时持有不是判断值是否为nil。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。IsValid()
常被用于判定返回值是否有效。
// 例子
func main() {
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // nil IsValid: false
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
// FieldByName方法是根据给定字符串返回字符串对应的结构体字段的信息,下面的结构体反射会讲解
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 不存在的结构体成员: false
// 尝试从结构体中查找"abc"方法
// MethodByName方法是根据方法名返回该类型方法集中的方法,下面的结构体反射会讲解
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // 不存在的结构体方法: false
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("abc")).IsValid()) // map中不存在的键: false
}
伍 结构体反射
5.1 StructField类型
StructField
类型用来描述结构体中的一个字段的信息,该类型是描述结构体中非常重要的类型,StructField
的定义如下:
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}
5.2 与结构体相关的方法
任意值通过reflect.TypeOf()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和Field()
方法获得结构体成员的详细信息。
reflect.Type
中与获取结构体成员相关的的方法如下表所示。
方法名以及返回类型 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 |
NumField() int | 返回结构体成员字段数量。 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据传入的匹配函数匹配需要的字段。 |
NumMethod() int | 返回该类型的方法集中方法的数目 |
Method(int) Method | 返回该类型方法集中的第i个方法 |
MethodByName(string) Method | 根据方法名返回该类型方法集中的方法 |
// 例子
package main
import (
"fmt"
"reflect"
)
type student struct {
Name string `json:"name"`
Gender string `json:"gender"`
Age int `json:"age"`
}
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
msg := "好好学习,天天向上。"
fmt.Println(msg)
return msg
}
func (s student) Sleep() string {
msg := "好好睡觉,快快长大。"
fmt.Println(msg)
return msg
}
func main() {
// 定义一个匿名结构体并初始化
stu1 := student{
Name: "A7cc",
Gender: "男",
Age: 18,
}
// 属性-----------------------
t := reflect.TypeOf(stu1)
// 使用NumField方法返回结构体成员字段数量
// 通过for循环遍历结构体的所有字段信息
for i := 0; i < t.NumField(); i++ {
// 使用Field方法根据索引,返回索引对应的结构体字段的信息
field := t.Field(i)
fmt.Printf("结构体字段名字:%s 结构体字段索引:%d 结构体字段类型:%v 结构体字段的json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
// 使用FieldByName方法通过字段名获取指定结构体字段信息
if ageField, ok := t.FieldByName("Age"); ok {
fmt.Printf("结构体字段名字:%s 结构体字段索引:%d 结构体字段类型:%v 结构体字段的json tag:%v\n", ageField.Name, ageField.Index, ageField.Type, ageField.Tag.Get("json"))
}
// 方法------------------------
v := reflect.ValueOf(stu1)
// 使用NumMethod方法返回该类型的方法集中方法的数目
// 通过for循环遍历结构体的所有方法信息
for i := 0; i < v.NumMethod(); i++ {
// 使用Method方法可以返回该类型方法集中的第i个方法
fmt.Printf("结构体方法名字:%s\n", t.Method(i).Name)
fmt.Printf("结构体方法类型以及返回值类型:%s\n", v.Method(i).Type())
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
// Call调用结构体方法
v.Method(i).Call(args)
}
// 使用MethodByName方法根据方法名返回该类型方法集中的方法
fmt.Println(v.MethodByName("Sleep").Type())
}
陆 使用反射的建议
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
- a.基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
- b.大量使用反射的代码通常难以理解。
- c.反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。