Go语言之反射

反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中, 并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,井且有能力修改它们

Go 程序在运行期使用reflect 包访问程序的反射信息

、反射的类型对象

在Go 程序中,使用reflect. TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息

下面例子表示获取类型对象的过程,如图:

运行结果如下:

  • 第10行:通过reflect.TypeOf()取得变量arr1的类型对象
  • 第11行:通过typeOfArr1类型对象的成员函数,获取变量的类型名和种类(Kind)

1、理解反射的类型(Type)与种类(Kind )

在使用反射时, 需要首先理解类型( Type )和种类( Kind )的区别,编程中,使用最多的是类型,在反射中, 当需要区分一个大品种的类型时,就会用到种类(Kind), 例如,需要统一判断类型中的指针时,使用种类( Kind )信息就较为方便。

Go 程序中的类型( Type )指的是系统原生数据类型,如int 、string 、bool、float32 等,也包括使用type关键字自定义的类型,这些类型的名称就是其类型本身的名称,例如使用type A struct{}定义结构体时,A就是struct{]的类型

种类( Kind )指的是对象归属的品种,在reflect 包中有如下定义:

类型种类
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双精度浮点数
Complex6464 位复数类型
Complex128128 位复数类型
Array数组
Chan通道
Func函数
Interface接口
Map映射
Ptr指针
Slice切片
String字符串
Struct结构体
Unsafe Pointer底层指针

下面例子定义字符串类型变量,自定义类型的常量和一个结构体类型变量,分别打印类型名和种类,如图:

从上图可以看出,通过type定义的类型,类型名为自定义的值,种类为实际归属的数据类型,非type定义的,类型名和种类名相同,都是实际归属的数据类型

2、指针与指针指向的元素

Go 程序中对指针获取反射对象时,可以通过reflect.Elem()方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个“ * ” 操作,如图:

运行结果如下:

  • 第10行:ins为*cat类型的指针变量
  • 第11行:对指针变量获取反射类型信息
  • 第12行:输出类型名和种类,Go语言反射中的所有指针变量种类都是Ptr,指针变量类型名为空,不是*cat
  • 第13行:取指针类型的元素类型,也就是cat类型,操作不可以,不可以通过非指针类型获取指针类型
  • 第14行:再次输出结果

3、使用反射获取结构体的成员类型

任意值通过reflect. TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象( reflect. Type )的NumField()和Field()方法获得结构体成员的详细信息

下面例子中,实例化一个结构体并遍历其结构体成员,再通过reflect.Type的FieldByName()方法查找结构体中的指定名称的字段,如图:

  • 第9行:声明带有2个成员的cat结构体
  • 第11行:字段类型后面用反引号“开始和结尾的字符串被称为Tag(标签),一般用于给字段添加自定义信息, 方便其他模块根据信息进行不同功能的处理
  • 第13行:创建结构体实例
  • 第14行:获取结构体反射类型对象
  • 第15行:使用reflect.Type 类型的NumField()方法获得一个结构体类型共有多少个字段,如果类型不是结构体, 将会触发岩机
  • 第16行:reflect. Type 中的Field()方法和NurnField()一般都是配对使用,用来实现结构体成员的遍历操作
  • 第17行:使用reflect.Type 的Field()方法返回的结构不再是reflect.Type 而是StructField 结构体,Name和Tag都是其中的成员变量
  • 第19行:使用reflect.Type 的FieldByName()根据字段名查找结构体字段信息,catType表示返回的结构体字段信息, 类型为StructField, ok 表示是否找到结构体字段的信息
  • 第20行:使用StructField 中Tag 的Get()方法,根据Tag 中的名字进行信息获取

reflect.Type 的Field()方法返回StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Tag)等,,而且还可以通过StructField 的Type 宇段进一步获取结构体成员的类型信,StructField 的结构如下:

type StructField struct {
       Name string        //字段名         
       PkgPath string     //字段路径   
       Type Type          //字段反射类型对象
       Tag StructTag      //字段的结构体标签
       Offset uintptr     //字段在结构体中的相对偏移
       Index []int        //Type.FieldByindex 中的返回的索引值
       Anonymous bool     //是否为匿名字段
}
方法说明
Field(i int) StructField根据索引返回索引对应的字段信息,返回的是StructField结构体
NumField() int返回结构体成员字段数量,返回值为整型
FieldByName(name string) (StructField,bool)根据给定字符串返回字符串对应的结构体字段的信息,没有找到时bool返回false
FieldBylndex(index []int) StructField多层成员访问时,根据[]int提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值
FieldByNameFunc( match func(string)bool) (StructField,bool)根据匹配函数匹配需要的字段

上述表格中,当类型(或值)不是结构体或索引超界时将发生宕机

4、结构体标签(Struct Tag)—对结构体字段的额外信息标签

通过reflect.Type 获取结构体成员信息reflect.StructField 结构中的Tag 被称为结构体标签( Struct Tag )

JSON 、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping , 简称ORM )系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体, 可以通过反射获取到

Tag 在结构体字段后方书写的格式如下:

`keyl:"valuel" key2:"value2"`             //用反引号括起来

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键与值之间没有空格,键值对之间使用一个空格分隔

StructTag 拥有一些方法, 可以进行Tag 信息的解析和提取,如下所示:

  • func(tag StructTag) Get(key string) string : 根据Tag 中的键获取对应的值,例如`key1:”value1″ key2:”value2″` 的Tag 中, 可以传入“ key1”获得“value1″
  • func (tag StructTag) Lookup(key string) (value string, ok bool):根据Tag 中的键, 查询值是否存在

二、反射的值对象

反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值,Go 语言中使用reflect.Value 获取和设置变量的值

1、使用反射值对象包装任意值

Go 语言中, 使用reflect.ValueOf()函数获得值的反射值对象( reflect. Value ),格式如下:

value := reflect.ValueOf(rawValue)

reflect.ValueOf 返回reflect.Value 类型,包含有rawValue 的值信息。reflect.Value 与原值间可以通过值包装和值获取互相转化,reflect. Value 是一些反射操作的重要类型,如反射调用函数

下面例子表示从反射值对象(reflect.Value)中获取值的例子,如图:

运行结果如下:

  • 第9行:声明一个变量,类型为int,初始值为100
  • 第10行:获取变量a 的反射值对象,类型为reflect.Value,这个过程和reflect.TypeOf()类似
  • 第11行:将valueOfA 反射值对象以interface{}类型取出,通过类型断言转换为int类型并赋值给getA
  • 第12行:将valueOfA 反射值对象通过Int 方法,以int64 类型取出,通过强制类型转换,转换为原本的int类型

可以通过下面几种方法从反射值对象reflect.Value 中获取原值,如下:

方法名说明
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 )提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问

方法备注
Field(i int) Value根据索引,返回索引对应的结构体成员字段的反射值对象
NumField() int返回结构体成员字段数量
FieldByName(name string) Value根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值
FieldBylndex(index []int) Value多层成员访问时,根据[]int提供的每个结构体的字段索引,返回字段的值。没有找到时返回零值
FieldByNameFunc( match func(string) bool) Value根据匹配函数匹配需要的字段。找到时返回零值

上面表格中 当值不是结构体或索引超界时发生岩机

(1)、下面例子构造了一个结构体包含不同类型的成员,通过reflect.Value 提供的成员访问函数,可以获得结构体值的各种数据,如图:

  • 第8-14行:定义结构体,每个字段类型都不一样
  • 第17行:实例化结构体并包装为reflect.Value 类型,成员中包含一个*dummy 的实例
  • 第18行:获取结构体的字段数量
  • 第20行:获取索引为2 的字段值( float32 字段〉, 并且打印类型
  • 第21行:根据“ b ”字符串,查找到b 宇段的类型
  • 第22行:[]int{4,0}中的4 表示,在dummy 结构中索引值为4 的成员,也就是c,c的的类型为dummy ,也是一个结构体,因此使用[]int {4,0 }中的0继续在c值的基础上索引, 结构为dummy 中索引值为0 的a 字段, 类型为int

(2)、下面例子中,将结构体切片b传递给函数gong(),函数参数为空接口类型,可接受任意类型参数,

package main

import (
	"fmt"
	"reflect"
)

type def struct {
	Name string
	Pass string
}
type s struct {
	Name string
	Pass string
}

//接收任意参数,最终返回切片类型
func gong(bb interface{}) []s {

        //调用函数ifSlice判断传入的参数是不是一个切片
	val, ok := isSlice(bb)
	if !ok {
		return nil
	}

	//通过val.Len()获取反射值对象的长度
	sliceLen := val.Len()
	//创建一个新的结构体切片,s为上面定义的结构体,长度为sliceLen
	out := make([]s, sliceLen)
	for i := 0; i < sliceLen; i++ {
		//通过for循环获取val值的第i个元素
		value := val.Index(i)

                //获取value的类型
		type1 := value.Type()

                //判断此种类是不是结构体
		if type1.Kind() != reflect.Struct {
			panic("Kind is not struct")
		}

                //如果是结构体,调用FieldByName根据给定字符串获取字段对应信息
		out[i].Name = value.FieldByName("Name").String()
		out[i].Pass = value.FieldByName("Pass").String()
	}
	return out
}

//判断是否为切片,通过反射
func isSlice(arg interface{}) (val reflect.Value, ok bool) {
	//使用reflect.ValudOf获取反射值对象reflect.Value
	val = reflect.ValueOf(arg)
	//判断val的种类是不是切片
	if val.Kind() == reflect.Slice {
		ok = true
	}
	return
}
func main() {
	b := []def{
		{Name: "hello", Pass: "111"},
		{Name: "world", Pass: "888"},
	}
	result := gong(b)
	fmt.Println("结果为:",result)
}
  • val虽然是种类为切片,但是其类型为反射值对象,不可遍历
  • value的种类虽然已经是结构体,但是不可直接遍历,必须通过FieldByName

注意:上述例子中将两个结构体定义在一个文件中,实际应用的的主要方式是向其他文件中传递结构体切片,并且新的结构体s和函数gong都要定义在其余文件中,进行处理传递过去的结构体切片并返回处理之后的,比如进行字段加密后返回

3、反射对象的空和有效性判断

反射值对象( reflect.Value )提供一系列方法进行零值和空判定,如下:

方法说明
IsNil() bool返回值是否为nil 。如果值类型不是通道(channel) 、函数、接口、map 、指针或切片时发生panic
IsValid() bool判断值是否有效,当值本身非法时,返回false ,例如reflect Value不包含任何值,值为nil 等

下面例子对各种方式的空指针进行IsNil 和IsValid 的返回值判定检测,同时对结构体成员及方法查找map 键值对的返回值进行IsValid 判定,如图:

  • 第9行:声明一个*int 类型的指针,初始值为nil
  • 第10行:将变量a 包装为reflect.Value 并且判断是否为空,此时变量a 为空指针,因此返回true
  • 第11行:对nil 进行IsValid (有效性)判定,返回false
  • 第12行:(*int)(nil)的含义是将nil 转换为*int ,也就是*int 类型的空指针,此行将nil 转换为*int 类型,并取指针指向元素,由于nil 不指向任何元素,*int 类型的nil也不能指向任何元素,值不是有效的。因此这个反射值使用Isvalid 判断时返回false
  • 第13行:实例化一个结构体
  • 第14行:通过FieldByName 查找s 结构体中一个空字符串的成员,如成员不存在,IsValid 返回false
  • 第15行:通过MethodByName 查找s 结构体中一个空字符串的方法,如方法不存在,IsValid 返回false
  • 第16行:实例化一个map ,这种写法与make 方式创建的map等效
  • 第17行:Maplndex()方法能根据给定的reflect. Value 类型的值查找map ,并且返回查找到的结果

IsNil 常被用于判断指针是否为空; IsValid 常被用于判定返回值是否有效

4、使用反射值对象修改变量的值

使用reflect.Value 对包装的值进行修改时, 需要遵循一些规则,如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生岩机

使用reflect.Value 取元素、取地址及修改值的属性方法如下:

方法名备注
Elem() Value取值指向的元素值,类似于语言层”*”操作。当值类型不是指针或接口时发生宕机,空指针时返回nil的Value
Addr() Value对可寻址的值返回其地址,类似于语言层“&”操作。当值不可寻址时发生宕机
CanAddr() bool表示值是否可寻址
CanSet() bool返回值能否被修改,要求值可寻址且是导出的字段

使用reflect.Value 修改值的相关方法如下:

Set( x Value)将值设置为传入的反射值对象的值
Setlnt(x int64)使用int64设置值。当值的类型不是mt 、int8 、intl6 、int32 、int64时会发生宕机
SetUint(x uint64)使用uint64设置值。当值的类型不是uint、uint8 、uintl6 、uint32 , uint64时会发生宕机
SetFloat(x float64)使用float64设置值。当值的类型不是float32 、float64时会发生宕机
SetBool(x bool)使用booI设置值,当值的类型不是bool时会发生宕机
SetBytes(x [)byte)设置字节数组[]bytes值。当值的类型不是[]byte时会发生宕机
SetString(x string)设置字符串值。当值的类型不是string时会发生宕机

以上方法,在reflect. Value 的CanSet 返回false 仍然修改值时会发生宕机,在己知值的类型时,应尽量使用值对应类型的反射设置值。

值可修改条件之一:可被寻址

通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改,如图:

值可修改条件之一:被导出

结构体成员中,如果字段没有被导出, 即便不使用反射也可以被访问,但不能通过反射修改,如图:

注意:上面例子中,如果字段首字母不大写,就会无法导出字段,会提示错误panic: reflect: reflect.Value.SetInt using value obtained using unexported field,如果第12行不通过结构体指针类型获取反射值对象,会报错panic: reflect: reflect.Value.SetInt using unaddressable value

值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”,修改变量值,一般的步骤是:

  • 取这个变量的地址或者这个变量所在的结构体已经是指针类型
  • 使用reflect.ValueOf 进行值包装
  • 通过Value.Elem()获得指针值指向的元素值对象(Value)因为值对象(Value),内部对象为指针时, 使用set设置时会报出岩机错误
  • 使用Value.Set 设置值

5、通过类型创建类型的实例

当己知reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针

例如:reflect.Type 的类型为int 时’, 创建int 的指针, 即 *int,如图:

  • 第12行:使用reflect.New()函数传入变量a 的反射类型对象,创建这个类型的实例值,值以reflect.Value 类型返回。这步操作等效于: new(int) ,因此返回的是*int 类型的实例
  • 第13行:打印aIns 的类型为 *int , 种类为指针。

6、使用反射调用函数

如果反射值对象( reflect.Value )中值的类型为函数时,可以通过reflect. Value 调用该函数,使用反射调用函数时,需要将参数使用反射值对象的切片[]reflect.Value 构造后传入Call()方法中,调用完成时,函数的返回值通过[]reflect.Value 返回

下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和,将函数保存到反射值对象( reflect.Value )中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用Call()方法进行调用,如图:

  • 第8行:定义普通的加法函数
  • 第12行:将add函数包装为反射值对象
  • 第13行:将10和100两个整数用reflect.ValueOf包装为reflect.Value,再将反射值对象的切片[]reflect.Value作为函数参数
  • 第14行:使用funcValue 函数值对象的Call()方法,传入参数列表paramLists 调用add()函数
  • 第15行:调用成功后, 通过resultList[0]取返回值的第一个参数,使用Int 取返回值的整数值

注意:反射调用函数的过程需要构造大量的reflect.Value 和中间交量, 对函数参数值进行逐一检查, 还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为reflect.Value ,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。

三、将结构体的数据保存为JSON格式的文本数据

在Go 语言中, 结构体可以通过系统提供的json.Marshal()函数进行序列化

将结构体序列化为JSON 的步骤如下:

  • 准备数据结构体
  • 准备要序列化的结构体数据
  • 调用序列化函数

下面的例子表示将结构体序列化为json格式,如图:

标签