Golang中reflect反射

1. 概述

在golang中,reflect是一个比较高级的话题,本文将尽可能简单而又清楚的介绍相关内容。

本文将从如下几个方面进行阐述:

  • 什么是reflection ?
  • 如何检查一个变量var 并获得其真实类型
  • golang中的reflect包
    • reflect.Type 和 reflect.Value
    • reflect.Kind
    • Int() 和 String()
    • NumField() 和 Field()
    • reflect.Elem
    • reflect.StructField
    • 完整实例
  • 使用reflection原则

2. 什么是reflection

简单的来说反射就是程序在运行时,通过检查其定义的变量以及值,进而找到其对应的真实类型。这样的定义可能很难让人理解,这里不需要担心。
接下来开始让我们根据后面的内容真正的理解reflection(反射)

3. 为什么需要通过检查一个变量并获得其真正的类型

在很多语言中都会存在反射或听说过反射,其中有一个问题在学习reflection(反射)过程中必然产生:为什么在程序运行时需要检查一个变量并获得其对应的类型type?产生的疑问的缘由:
在我们的程序中任意一个定义的变量,其类型都是在编译时是可知的。大多数情况下这样的结论是正确的,但不代表全部。
看看下面的一个简单实例:

运行程序输出结果:

在上面的实例中i的类型其实在编译时已是可知的,当打印出结果时并没有什么神奇的情况出现。
接下来让我们看看运行时的变量类型,首先定义一个function并用一个struct作为形参传递给fmt.Println。

当运行程序后,输出结果:

接下来让我们来改造下上面这个例子

运行程序输出结果:

接着我们继续对实例进行改造,让query参数能够适配任意struct:

在上面的实例发现我们将query的接收参数类型:interface{}为了接收任意类型的struct。
例如:当我们提供一个order的结构体时:

对应的query返回的输出内容sql:

那么当我们提供employee的结构体时

应该返回的内容sql:

在上面的例子通过提供一个interface{}给query函数能够满足任意struct的处理。(实例也是为了简单起见,提供的struct只包含了string和int类型,不过可以扩展其他类型)
对于query函数来说为了适配任意类型的struct,通过在运行时检查传递的struct的具体类型并根据其包含的字段来生成对应的sql。在这里reflection(反射)是相当有用的。接下来让我们使用golang的reflect包来解决这种情况

4. reflect包

在golang中,reflect包是用来实现运行反射的。通过reflect包能够完成对一个interface{}变量的具体类型以及值的获取。在query函数提供了一个interface{}类型参数并根据interface{}具体的类型以及值生成对应的sql,reflect包对我们来说相对比较需要的并能很好解决该问题。

4.1. reflect.Type和reflect.Value

interface{}类型变量其具体类型可以使用reflect.Tpye来表示,而其具体值则使用reflect.Value来表示。而reflect.Type和reflect.Value分别提供reflect.TypeOf()和reflect.ValueOf()来获取interface{}的具体类型及具体值。接下来我们结合例子来进行说明

输出内容:

从输出的结果可以看出程序输出了interface{}的真实类型以及真实值。

4.2. reflect.Kind

在reflect还有一个比较重要的类型Kind,也是代表类型,看起来和我们前面提到的reflect.Type很相似,其实两者有着很大的差异:

输出结果:

通过输出结果我们能够很清楚的看出来reflect.Type和reflect.Kind:Type代表interface{}实际类型main.order;而Kind代表具体类型struct。(读起来有点绕口...)

4.3. Int() 和String()

Int()和String()主要用于从reflect.Value提取对应值作为int64和string类型

输出结果:

4.4. NumField() 和Field()

NumField()方法获取一个struct所有的fields,Field(i int)获取指定第i个field的reflect.Value,结合具体实例:

输出结果:

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

4.4.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) 根据匹配函数匹配需要的字段。当值不是结构体或索引超界时发生宕机

4.4.2. Field实例

输出结果:

4.5. reflect.Elem

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

代码输出如下:

4.6. reflect.StructField

4.6.1. StructField结构体

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

字段说明如下

  • Name:为字段名称。
  • PkgPath:字段在结构体中的导入唯一标识该包的路径,例如"encoding、base64"。如果类型已经被宣告(string,error)或是未命名 (*T, struct{}, []int),路径将是空字符串。
  • Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
  • Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
  • Index:FieldByIndex 中的索引顺序。
  • Anonymous:表示该字段是否为匿名字段。

4.6.2. 获取成员反射信息

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

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

代码输出如下:

4.7. 使用实例

5. 如何使用reflection(反射)

前面已经演示了reflection(反射)的使用,那真正的问题:该如何应用反射?

Clear is better than clever. Reflection is never clear.--- Rob Pike

reflection(反射)在golang中比较强大和高级的特性,使用时是需要注意的。因为reflection(反射)很难实现清晰并可维护的代码。遵循一条:尽量避免使用,除非方案必须使用。reflection(反射)的推导是比较复杂的,并不是为了随意使用而设计的。

  • 反射的三大定律
  1. interface{}类型的值到反射reflecton对象.
    根源上来说, reflection的原理就是检查interface中保存的一对值和类型, 所以在reflect包中,有两个类型我们需要记住, Type和Value两个类型. 通过这两个类型,我们可以访问一个interface变量的内容. 调用reflect.ValueOf和reflect.TypeOf可以检索出一个interface的值和具体类型. 当然通过reflect.Value我们也可以获得reflect.Type。
  2. 反射reflection对象到interface{}类型的值.
    通过reflect.Value的Interface方法,我们可以获得一个Interface值。实际上这个方法将一个type和value打包回interface
  3. 当修改一个反射reflection时, 其值必须是settable.(留个问题:如何通过reflect.Value来设置一个值,并需要注意哪些事宜)。
赞赏

微信赞赏支付宝赞赏

发表评论

邮箱地址不会被公开。 必填项已用*标注