反射

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

C/C++语言没有支持反射功能,只能通过typeid提供非常弱化的程序运行时类型信息。Java、C#等语言都支持完整的反射功能。Lua、JavaScript类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。Go程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。

反射的类型对象(reflect.Type)

在Go程序中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。下面通过例子来理解获取类型对象的过程:

package main

import (
"fmt"
"reflect"
) func main() { var a int typeOfA := reflect.TypeOf(a) fmt.Println(typeOfA.Name(), typeOfA.Kind()) }

  

代码输出如下:

int  int

  

代码说明如下:

  • 第10行,定义一个int类型的变量。
  • 第12行,通过reflect.TypeOf()取得变量a的类型对象typeOfA,类型为reflect.Type()。
  • 第14行中,通过typeOfA类型对象的成员函数,可以分别获取到typeOfA变量的类型名为int,种类(Kind)为int。

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

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

1.反射种类(Kind)的定义

Go程序中的类型(Type)指的是系统原生数据类型,如int、string、bool、float32等类型,以及使用type关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用typeAstruct{}定义结构体时,A就是struct{}的类型。种类(Kind)指的是对象归属的品种,在reflect包中有如下定义:

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 // 底层指针
)

  

Map、Slice、Chan属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于Ptr。type A struct{}定义的结构体属于Struct种类,*A属于Ptr。

2.从类型对象中获取类型名称和种类的例子

Go语言中的类型名称对应的反射获取方法是reflect.Type中的Name()方法,返回表示类型名称的字符串。类型归属的种类(Kind)使用的是reflect.Type中的Kind()方法,返回reflect.Kind类型的常量。下面的代码中会对常量和结构体进行类型信息获取。

package main

import (
"fmt"
"reflect"
) // 定义一个Enum类型
type Enum int const (
Zero Enum = 0
) func main() { // 声明一个空结构体
type cat struct {
} // 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(cat{}) // 显示反射类型对象的名称和种类
fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // 获取Zero常量的反射类型对象
typeOfA := reflect.TypeOf(Zero) // 显示反射类型对象的名称和种类
fmt.Println(typeOfA.Name(), typeOfA.Kind()) }

  

代码输出如下:

cat struct
Enum int

  

代码说明如下:

  • 第18行,声明结构体类型cat。
  • 第22行,将cat实例化,并且使用reflect.TypeOf()获取被实例化后的cat的反射类型对象。
  • 第25行,输出cat的类型名称和种类,类型名称就是cat,而cat属于一种结构体种类,因此种类为struct。
  • 第28行,Zero是一个Enum类型的常量。这个Enum类型在第9行声明,第12行声明了常量。如没有常量也不能创建实例,通过reflect.TypeOf()直接获取反射类型对象。
  • 第31行,输出Zero对应的类型对象的类型名和种类。

指针与指针指向的元素

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

package main

import (
"fmt"
"reflect"
) func main() { // 声明一个空结构体
type cat struct {
} // 创建cat的实例
ins := &cat{} // 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins) // 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind()) // 取类型的元素
typeOfCat = typeOfCat.Elem() // 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind()) }

  

代码输出如下:

name:'' kind:'ptr'
element name: 'cat', element kind: 'struct'

  

代码说明如下:

  • 第15行,创建了cat结构体的实例,ins是一个*cat类型的指针变量。
  • 第18行,对指针变量获取反射类型信息。
  • 第21行,输出指针变量的类型名称和种类。Go语言的反射中对所有指针变量的种类都是Ptr,但注意,指针变量的类型名称是空,不是*cat。
  • 第24行,取指针类型的元素类型,也就是cat类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
  • 第27行,输出指针变量指向元素的类型名称和种类,得到了cat的类型名称(cat)和种类(struct)。

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

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

表1-1   结构体成员访问的方法列表
方法 说明
Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。当值不是结构体或索引超界时发生宕机
NumField() int 返回结构体成员字段数量。当类型不是结构体或索引超界时发生宕机
FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。没有找到时bool返回 false,当类型不是结构体或索引超界时发生宕机
FieldByIndex(index []int) StructField 多层成员访问时,根据[]int 提供的每个结构体的字段索引,返回字段的信息。没有找到时返回零值。当类型不是结构体或索引超界时发生宕机
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据匹配函数匹配需要的字段。当值不是结构体或索引超界时发生宕机

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

type StructField struct {
Name string // 字段名
PkgPath string // 字段路径
Type Type // 字段反射类型对象
Tag StructTag // 字段的结构体标签
Offset uintptr // 字段在结构体中的相对偏移
Index []int // Type.FieldByIndex中的返回的索引值
Anonymous bool // 是否为匿名字段
}

  

字段说明如下。

  • Name:为字段名称。
  • PkgPath:字段在结构体中的路径。
  • Type:字段本身的反射类型对象,类型为reflect.Type,可以进一步获取字段的类型信息。
  • Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
  • Index:FieldByIndex中的索引顺序。
  • Anonymous:表示该字段是否为匿名字段。

2.获取成员反射信息

下面代码中,实例化一个结构体并遍历其结构体成员,再通过reflect.Type的FieldByName()方法查找结构体中指定名称的字段,直接获取其类型信息。

反射访问结构体成员类型及信息:

package main

import (
"fmt"
"reflect"
) func main() { // 声明一个空结构体
type cat struct {
Name string // 带有结构体tag的字段
Type int `json:"type" id:"100"`
} // 创建cat的实例
ins := cat{Name: "mimi", Type: 1} // 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins) // 遍历结构体所有成员
for i := 0; i < typeOfCat.NumField(); i++ { // 获取每个成员的结构体字段类型
fieldType := typeOfCat.Field(i) // 输出成员名和tag
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
} // 通过字段名, 找到字段类型信息
if catType, ok := typeOfCat.FieldByName("Type"); ok { // 从tag中取出需要的tag
fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
}
}

  

代码输出如下:

name: Name  tag: ''
name: Type tag: 'json:"type" id:"100"'
type 100

  

代码说明如下:

  • 第11行,声明了带有两个成员的cat结构体。
  • 第15行,Type是cat的一个成员,这个成员类型后面带有一个以`开始和结尾的字符串。这个字符串在Go语言中被称为Tag(标签)。一般用于给字段添加自定义信息,方便其他模块根据信息进行不同功能的处理。
  • 第19行,创建cat实例,并对两个字段赋值。结构体标签属于类型信息,无须且不能赋值。
  • 第22行,获取实例的反射类型对象。
  • 第25行,使用reflect.Type类型的NumField()方法获得一个结构体类型共有多少个字段。如果类型不是结构体,将会触发宕机错误。
  • 第28行,reflect.Type中的Field()方法和NumField一般都是配对使用,用来实现结构体成员的遍历操作。
  • 第31行,使用reflect.Type的Field()方法返回的结构不再是reflect.Type而是StructField结构体。
  • 第35行,使用reflect.Type的FieldByName()根据字段名查找结构体字段信息,catType表示返回的结构体字段信息,类型为StructField,ok表示是否找到结构体字段的信息。
  • 第38行中,使用StructField中Tag的Get()方法,根据Tag中的名字进行信息获取。

结构体标签(Struct Tag)

通过reflect.Type获取结构体成员信息reflect.StructField结构中的Tag被称为结构体标签(StructTag)。结构体标签是对结构体字段的额外信息标签。

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

1.结构体标签的格式

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

`key1:"value1" key2:"value2"`

  

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

2.从结构体标签中获取值

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中的键,查询值是否存在。

3.结构体标签格式错误导致的问题

编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,参见下面这个例子:

package main

import (
"fmt"
"reflect"
) func main() { type cat struct {
Name string
Type int `json: "type" id:"100"`
} typeOfCat := reflect.TypeOf(cat{}) if catType, ok := typeOfCat.FieldByName("Type"); ok { fmt.Println(catType.Tag.Get("json"))
} }

  

代码输出空字符串,并不会输出期望的type。第12行中,在json:和"type"之间增加了一个空格。这种写法没有遵守结构体标签的规则,因此无法通过Tag.Get获取到正确的json对应的值。这个错误在开发中非常容易被疏忽,造成难以察觉的错误。所以,修改上述代码第12行为如下代码,则可以正常打印。

type cat struct {
Name string
Type int `json:"type" id:"100"`
}

  

Go语言之反射(一)的更多相关文章

  1. go语言通过反射获取和设置结构体字段值的方法

    本文实例讲述了go语言通过反射获取和设置结构体字段值的方法.分享给大家供大家参考.具体实现方法如下: type MyStruct struct { N int } n := MyStruct{ 1 } ...

  2. 深度解密Go语言之反射

    目录 什么是反射 为什么要用反射 反射是如何实现的 types 和 interface 反射的基本函数 反射的三大定律 反射相关函数的使用 代码样例 未导出成员 反射的实际应用 json 序列化 De ...

  3. Go语言 8 反射

    文章由作者马志国在博客园的原创,若转载请于明显处标记出处:http://www.cnblogs.com/mazg/ Go学习群:415660935 8.1概念和作用 Reflection(反射)在计算 ...

  4. Go语言之反射(二)

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

  5. Go语言的反射

    反射是语言里面是非常重要的一个特性,我们经常会看见这个词,但是对于反射没有一个很好的理解,主要是因为对于反射的使用场景不太熟悉. 一.理解变量的内在机制 1.类型信息,元信息,是预先定义好的,静态的. ...

  6. 列举java语言中反射的常用方法

    package review;/*12:43 2019/7/21*/ import model.AnotherClass; import model.OneClassMore; import mode ...

  7. Go语言之反射(三)

    结构体转JSON JSON格式是一种用途广泛的对象文本格式.在Go语言中,结构体可以通过系统提供的json.Marshal()函数进行序列化.为了演示怎么样通过反射获取结构体成员以及各种值的过程,下面 ...

  8. go语言之反射

    ---恢复内容开始--- 一 :并发基础 1 并发和并行 并发和并行是两个不同的概念: 并行意味着程序在任意时刻都是同时运行的: 并发意味着程序在单位时间内是同时运行的 详解: 并行就是在任一粒度的时 ...

  9. Go语言基础之反射

    Go语言基础之反射 本文介绍了Go语言反射的意义和基本使用. 变量的内在机制 Go语言中的变量是分为两部分的: 类型信息:预先定义好的元信息. 值信息:程序运行过程中可动态变化的. 反射介绍 反射是指 ...

随机推荐

  1. Android数据库(1)、SQLite数据库介绍

    一.关系性数据库   关系型数据库主要有以下三个特征,尤为明显,如果没有这个三个特征约束,当多个客户端使用数据的时候就会出现各种各样的错误,所以关系型数据库定义这些约束,让客户端程序只要遵守这个规则便 ...

  2. World Wind Java开发之四——搭建本地WMS服务器(转)

    在提供地理信息系统客户端时,NASA还为用户提供了开源的WMS Server 服务器应用:World Wind WMS Server.利用这个应用,我们可以架设自己的WMS服务并使用自己的数据(也支持 ...

  3. go语言,爬取百度贴吧指定贴所有内容

    初级爬虫,为了学习一下常用的goquery. goquery 配置 go get https://github.com/PuerkitoBio/goquery 会提示不支持https方式 解决方案: ...

  4. python_63_装饰器6

    #decorator意思:1.装饰器 2.语法糖 import time user,passwd='qi','123' def auth(func): def wrappper(*args, **kw ...

  5. 题解 P2626 【斐波那契数列(升级版)】

    这道题,大家一定要注意: 要对2^31取模 ! ( 本蒟蒻开始没注意到这一点,WA了 ) (不过大家在试样例的时候,试试47,出不了结果,就说明你没模2^31) 总体来说,这道题考查的知识点就两个: ...

  6. CUDA中记录执行时间-GPU端

    事件eventcudaEvent_t start,stop;cudaEventCreate(&start);cudaEventCreate(&stop);cudaEventRecord ...

  7. cuda中当元素个数超过线程个数时的处理案例

    项目打包下载 当向量元素超过线程个数时的情况 向量元素个数为(33 * 1024)/(128 * 128)=2.x倍 /* * Copyright 1993-2010 NVIDIA Corporati ...

  8. Jquery LigerUI

    http://www.ligerui.com/index.aspx http://api.ligerui.com/ 分享一个基于ligerui的系统应用案例ligerRM V2(权限管理系统)(提供下 ...

  9. C#图片上传获取二进制流保存至AD

    <form id="form1" runat="server"> <div> <asp:FileUpload ID="F ...

  10. MitmProxy使用

    安装 tar -zxvf mitmproxy-3.0.1-linux.tar.gz sudo mv mitmproxy mitmdump mitmweb /usr/bin 详情 https://ger ...