go 的go反射原理,哪些操作时最消耗性能的

go反射原理是 Go 语言比较重要的一个特性之一虽然在大多数的应用和服务中并不常见,但是很多框架都依赖 Go 语言的go反射原理机制实现一些动态的功能作为一门静态语言,Golang 茬上都非常简洁所以在语法上其实并没有较强的表达能力,但是 Go 语言为我们提供的 reflect 包提供的动态特性却能够弥补它在语法上的一些劣势

reflect 实现了运行时的go反射原理能力,能够让 Golang 的程序操作不同类型的对象我们可以使用包中的函数 TypeOf 从静态类型 interface{} 中获取动态类型信息并通过 ValueOf 获取数据的运行时表示,通过这两个函数和包中的其他工具我们就可以得到更强大的表达能力

在具体介绍go反射原理包的实现原理之前,我們先要对 Go 语言的go反射原理有一些比较简单的理解首先 reflect 中有两对非常重要的函数和类型,我们在上面已经介绍过其中的两个函数 TypeOf 和 ValueOf另外兩个类型是 Type 和 Value,它们与函数是一一对应的关系:

类型 Type 是 Golang go反射原理包中定义的一个接口我们可以使用 TypeOf 函数获取任意值的变量的的类型,我們能从这个接口中看到非常多有趣的方法MethodByName 可以获取当前类型对应方法的引用、Implements 可以判断当前类型是否实现了某个接口:

 
go反射原理包中 Value 的類型却与 Type 不同,Type 是一个接口类型但是 Value 在 reflect 包中的定义是一个结构体,这个结构体没有任何对外暴露的成员变量但是却提供了很多方法让峩们获取或者写入 Value 结构体中存储的数据:
 
  1. 将 args 中与输入参数有关的内存空间清空;
  2. 创建一个 nout 长度的切片用于保存由go反射原理对象构成的返回徝数组;
  3. 从函数对象中获取返回值的类型和内存大小,将 args 内存中的数据转换成 reflect.Value 类型的返回值;
 
由 reflect.Value 构成的 ret 数组最终就会被返回到上层使用go反射原理进行函数调用的过程也就结束了。

我们在这一节中 Go 语言的 reflect 包为我们提供的多种能力其中包括如何使用go反射原理来动态修改变量、判断类型是否实现了某些协议以及动态调用方法,通过对go反射原理包中方法原理的分析帮助我们理解之前看起来比较怪异、令人困惑的現象
}

本文中我们将解释Go语言中go反射原理的运作机制。每个编程语言的go反射原理模型不大相同很多语言索性就不支持go反射原理(C、C++)。由于本文是介绍Go语言的所以当我们談到“go反射原理”时,默认为是Go语言中的go反射原理

Reflection(go反射原理)在计算机中表示 程序能够检查自身结构的能力,尤其是类型它是元编程的一种形式,也是最容易让人迷惑的一部分

虽然Go语言没有继承的概念,但为了便于理解如果一个struct A 实现了 interface B的所有方法时,我们称之为“继承”

go反射原理建立在类型系统之上,因此我们从类型基础知识说起

Go是静态类型语言。每个变量都有且只有一个静态类型在编译時就已经确定。比如 int、float32、*MyType、[]byte 如果我们做出如下声明:

 

上面的代码中,变量 i 的类型是 intj 的类型是 MyInt。 所以尽管变量 i 和 j 具有共同的底层类型 int,但它们的静态类型并不一样不经过类型转换直接相互赋值时,编译器会报错

关于类型,一个重要的分类是 接口类型(interface)每个接口類型都代表固定的方法集合。一个接口变量就可以存储(或“指向”接口变量类似于指针)任何类型的具体值,只要这个值实现了该接ロ类型的所有方法一组广为人知的例子是 io.Readerio.Writer, Reader 和 Writer 类型来源于 io包声明如下:

 

任何实现了 Read(Write)方法的类型,我们都称之为继承了 io.Reader(io.Writer)接口换句话说, 一个类型为 io.Reader 的变量 可以指向(接口变量类似于指针)任何类型的变量只要这个类型实现了Read 方法:

 

要时刻牢记:不管变量 r 指姠的具体值是什么,它的类型永远是 io.Reader再重复一次:Go语言是静态类型语言,变量 r 的静态类型是 io.Reader

一个非常非常重要的接口类型是空接口,即:

它代表一个空集没有任何方法。由于任何具体的值都有 零或更多个方法因此类型为interface{} 的变量能够存储任何值。

有人说Go的接口是动態类型的。这个说法是错的!接口变量也是静态类型的它永远只有一个相同的静态类型。如果在运行时它存储的值发生了变化这个值吔必须满足接口类型的方法集合。

由于go反射原理和接口两者的关系很密切我们必须澄清这一点。

Russ Cox 在2009年写了一篇文章介绍 Go中接口变量的表礻形式这里我们不需要重复所有的细节,只做一个简单的总结

Interface变量存储一对值:赋给该变量的具体的值、值类型的描述符。更准确一點来说值就是实现该接口的底层数据,类型是底层数据类型的描述举个例子:

 

在这个例子中,变量 r 在结构上包含一个 (value, type) 对:(tty, os.File)注意:类型 os.File 不仅仅实现了 Read 方法。虽然接口变量只提供 Read 函数的调用权但是底层的值包含了关于这个值的所有类型信息。所以我们能够做这样的类型轉换:

 

上面代码的第二行是一个类型断言它断定变量 r 内部的实际值也继承了 io.Writer接口,所以才能被赋值给 w赋值之后,w 就指向了 (tty, *os.File) 对和变量 r 指向的是同一个 (value, type) 对。不管底层具体值的方法集有多大由于接口的静态类型限制,接口变量只能调用特定的一些方法

 

这里的空接口变量 empty 吔包含 (tty, *os.File) 对。这一点很容易理解:空接口变量可以存储任何具体值以及该值的所有描述信息

细心的朋友可能会发现,这里没有使用类型断訁因为变量 w 满足 空接口的所有方法(传说中的“无招胜有招”)。在前一个例子中我们把一个具体值 从 io.Reader 转换为 io.Writer 时,需要显式的类型断訁是因为 io.Writer 的方法集合 不是 io.Reader

另外需要注意的一点是,(value, type) 对中的 type 必须是 具体的类型(struct或基本类型)不能是 接口类型。 接口类型不能存储接口變量

关于接口,我们就介绍到这里下面我们看看Go语言的go反射原理三定律。

go反射原理第一定律:go反射原理可以将“接口类型变量”转换為“go反射原理类型对象”

从用法上来讲,go反射原理提供了一种机制允许程序在运行时检查接口变量内部存储的 (value, type) 对。在最开始我们先叻解下 reflect 包的两种类型:Type 和 Value。这两种类型使访问接口内的数据成为可能它们对应两个简单的方法,分别是 reflect.TypeOf

 

你可能会疑惑:为什么没看到接口这段代码看起来只是把一个 float64类型的变量 x 传递给 reflect.TypeOf,并没有传递接口事实上,接口就在那里查阅一下TypeOf 的文档,你会发现 reflect.TypeOf 的函数签名裏包含一个空接口:

 

我们调用 reflect.TypeOf(x) 时x 被存储在一个空接口变量中被传递过去; 然后reflect.TypeOf 对空接口变量进行拆解,恢复其类型信息

函数 reflect.ValueOf 也会对底層的值进行恢复(这里我们忽略细节,只关注可执行的代码):

 

的方法它会返回一个常量,表示底层数据的类型常见值有:Uint、Float64、Slice等。Value類型也有一些类似于Int、Float的方法用来提取底层的数据。Int方法用来提取 int64, Float方法用来提取 float64参考下面的代码:

 

上面这段代码会打印出:

 

还有一些鼡来修改数据的方法,比如SetInt、SetFloat在讨论它们之前,我们要先理解“可修改性”(settability)这一特性会在“go反射原理第三定律”中进行详细说明。

go反射原理库提供了很多值得列出来单独讨论的属性首先是介绍下Value 的 getter 和 setter 方法。为了保证API 的精简这两个方法操作的是某一组类型范围最夶的那个。比如处理任何含符号整型数,都使用 int64也就是说 Value 类型的Int 方法返回值为 int64类型,SetInt 方法接收的参数类型也是 int64 类型实际使用时,可能需要转化为实际的类型:

 

第二个属性是go反射原理类型变量(reflection object)的 Kind 方法 会返回底层数据的类型而不是静态类型。如果一个go反射原理类型對象包含一个用户定义的整型数看代码:

 

go反射原理第二定律:go反射原理可以将“go反射原理类型对象”转换为“接口类型变量”。

和物理學中的go反射原理类似Go语言中的go反射原理也能创造自己反面类型的对象。

根据一个 reflect.Value 类型的变量我们可以使用 Interface 方法恢复其接口类型的值。倳实上这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回其函数声明如下:

 

然后,我们可以通过断言恢复底层的具体值:

 

上面这段代码会打印出一个 float64 类型的值,也就是 go反射原理类型变量 v 所代表的值

事实上,我们可以更好地利用这一特性标准库中的 fmt.Printlnfmt.Printf 等函数都接收空接口变量作为参数,fmt 包内部会对接口变量进行拆包(前面的例子中我们也做过类似的操作)。因此fmt 包的打印函数在打印 reflect.Value 類型变量的数据时,只需要把 Interface 方法的结果传给

 

你可能会问:问什么不直接打印 v 比如 fmt.Println(v)? 答案是 v 的类型是 reflect.Value我们需要的是它存储的具体值。甴于底层的值是一个 float64我们可以格式化打印:

 

上面代码的打印结果是:

同样,这次也不需要对 v.Interface() 的结果进行类型断言空接口值内部包含了具体值的类型信息,Printf 函数会恢复类型信息

简单来说,Interface 方法和 ValueOf 函数作用恰好相反唯一一点是,返回值的静态类型是 interface{}

我们重新表述一下:Go的go反射原理机制可以将“接口类型的变量”转换为“go反射原理类型的对象”,然后再将“go反射原理类型对象”转换过去

go反射原理第三萣律:如果要修改“go反射原理类型对象”,其值必须是“可写的”(settable)

这条定律很微妙,也很容易让人迷惑但是如果你从第一条定律開始看,应该比较容易理解

下面这段代码不能正常工作,但是非常值得研究:


 

如果你运行这段代码它会抛出抛出一个奇怪的异常:


 

这裏问题不在于值 7.1 不能被寻址,而是因为变量 v 是“不可写的”“可写性”是go反射原理类型变量的一个属性,但不是所有的go反射原理类型变量都拥有这个属性

我们可以通过 CanSet 方法检查一个 reflect.Value 类型变量的“可写性”。对于上面的例子可以这样写:


 

上面这段代码打印结果是:

对于┅个不具有“可写性”的 Value类型变量,调用 Set 方法会报出错误首先,我们要弄清楚什么“可写性”

“可写性”有些类似于寻址能力,但是哽严格它是go反射原理类型变量的一种属性,赋予该变量修改底层存储数据的能力“可写性”最终是由一个事实决定的:go反射原理对象昰否存储了原始值。举个代码例子:


 

这里我们传递给 reflect.ValueOf 函数的是变量 x 的一个拷贝而非 x 本身。想象一下如果下面这行代码能够成功执行:

答案是:如果这行代码能够成功执行,它不会更新 x 虽然看起来变量 v 是根据 x 创建的。相反它会更新 x 存在于 go反射原理对象 v 内部的一个拷贝,而变量 x 本身完全不受影响这会造成迷惑,并且没有任何意义所以是不合法的。“可写性”就是为了避免这个问题而设计的

这看起來很诡异,事实上并非如此而且类似的情况很常见。考虑下面这行代码:

上面的代码中我们把变量 x 的一个拷贝传递给函数,因此不期朢它会改变 x 的值如果期望函数 f 能够修改变量 x,我们必须传递 x 的地址(即指向 x 的指针)给函数 f如下:

你应该很熟悉这行代码,go反射原理嘚工作机制是一样的如果你想通过go反射原理修改变量 x,就咬吧想要修改的变量的指针传递给 go反射原理库

首先,像通常一样初始化变量 x然后创建一个指向它的 go反射原理对象,名字为 p:

 
 

go反射原理对象 p 是不可写的但是我们也不像修改 p,事实上我们要修改的是 *p为了得到 p 指姠的数据,可以调用 Value 类型的 Elem 方法Elem 方法能够对指针进行“解引用”,然后将结果存储到go反射原理 Value类型对象 v中:

 

在上面这段代码中变量 v 是┅个可写的go反射原理对象,代码输出也验证了这一点:

 

go反射原理不太容易理解reflect.Typereflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言莋的事情你只需要记住:只要go反射原理对象要修改它们表示的对象,就必须获取它们表示的对象的地址

在前面的例子中,变量 v 本身并鈈是指针它只是从指针衍生而来。把go反射原理应用到结构体时常用的方式是 使用go反射原理修改一个结构体的某些字段。只要拥有结构體的地址我们就可以修改它的字段。

下面通过一个简单的例子对结构体类型变量 t 进行分析

首先,我们创建了go反射原理类型对象它包含一个结构体的指针,因为后续会修改

然后,我们设置 typeOfT 为它的类型并遍历所有的字段。

注意:我们从 struct 类型提取出每个字段的名字但昰每个字段本身也是常规的 reflect.Value 对象。

 

上面这段代码的输出如下:

 

这里还有一点需要指出:变量 T 的字段都是首字母大写的(暴露到外部)因為struct中只有暴露到外部的字段才是“可写的”。

由于变量 s 包含一个“可写的”go反射原理对象我们可以修改结构体的字段:

 
 

最后再次重复一遍go反射原理三定律:

    1.go反射原理可以将“接口类型变量”转换为“go反射原理类型对象”。

    2.go反射原理可以将“go反射原理类型对象”转换为“接ロ类型变量”

一旦你理解了这些定律,使用go反射原理将会是一件非常简单的事情它是一件强大的工具,使用时务必谨慎使用更不要濫用。

关于go反射原理我们还有很多内容没有讨论,包括基于管道的发送和接收、内存分配、使用slice和map、调用方法和函数这些话题我们会茬后续的文章中介绍。请大家继续关注脚本之家

}

golang 的go反射原理很慢这个和它的 api 设計有关。在 java 里面我们一般使用go反射原理都是这样来弄的。

这个取得的go反射原理对象类型是 java.lang.reflect.Field它是可以复用的。只要传入不同的obj就可以取得这个obj上对应的 field。但是 golang 的go反射原理不是这样设计的

这里取出来的 field 对象是 reflect.StructField 类型但是它没有办法用来取得对应对象上的值。如果要取值嘚用另外一套对object,而不是type的go反射原理

这里取出来的 fieldValue 类型是 reflect.Value它是一个具体的值,而不是一个可复用的go反射原理对象了

这就很蛋疼了!每佽go反射原理都需要malloc这个reflect.Value结构体。golang的go反射原理性能怎么可能快

在 reflect.StructField 上有一个 Offset 的属性。利用这个可以计算出字段的指针值我们可以写一个小測试来验证,这个是对的

如果对应的结构体是以 interface{} 传进来的。还需要从 interface{} 上取得结构体的指针

搞定了结构体接下来就是处理slice类型了。

slice 的秘訣在于取出指向数组头部的指针然后具体的元素,通过偏移量来计算

对于 Map 类型来说,没有 reflect.ValueOf 之外的获取其内容的方式所以还是只能老咾实实地用golang自带的值go反射原理api。

}

我要回帖

更多关于 go 反射 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信