go中reflect的底层原理
在go语言中,reflect
包实现了运行时反射,反射是指获取对象在运行时的动态类型和原始对象,不同于静态类型,它是获取对象的底层对象。就相当于C++在运行时中对于多态保存的type_info
信息,可以根据这个实现dynamic_cast
。
go语言文档中说明,reflect
的典型用法是先将一个具有静态类型 interface{}
的值,传入 TypeOf
来提取它的动态类型信息,传入 ValueOf
则会返回一个表示该值在运行时数据的 Value
。所以reflect
在使用时一般都和空接口绑定使用,它们的语义吻合,空接口是一个静态类型,不知道里面存储的是什么类型的数据,通过反射就可以知道该空接口对象在运行时实际的类型,因为反射的目标就是在运行时处理类型未知的对象。
而且reflect
包中的获取运行时对象和值的方法的函数参数都是空接口:
go
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
可以看出go的反射机制是基于interface{}
实现的,因为任何类型的值都可以被放入interface{}
,所以我们要弄懂reflect
的底层,我们就必须弄清楚关于interface{}
的底层。关于接口的原理可以看一下这篇问答,下面我只简要带过一下。
空接口在我们的程序设计中一般都是来接收未知或者可变的数据,接口由两个字段构成,一个是指向该值所有的实际数据类型,一个是指向该值关于该接口的方法表,对于该方法表,它只会存储对应接口所声明的方法和该对象的静态类型信息。也可以理解为(type, value),其中型就是赋值给接口变量的值的类型,值就是赋值给接口变量的值。比如一个类型实现了Stringer
接口,那么在对应的方法表中,只列出了那些用于满足 Stringer
接口的方法,也就是 String
方法,而该对象实现的其他接口的方法则没有,比如什么Get()
方法。
并且Go 语言是静态类型的,每个变量在编译期有且只能有一个确定的、已知的类型,即变量的静态类型。静态类型在变量声明的时候就已经确定了,无法修改。一个接口变量,它的静态类型就是该接口类型。虽然在运行时可以将不同类型的值赋值给它,改变的也只是它内部的动态类型和动态值。它的静态类型始终没有改变。
由于我们知道接口存储了实际对象的值和类型信息,所以如果我们知道该接口对应的值的话,就可以直接使用类型断言来获取我们期待的类型。但是,如果类型断言的类型与实际存储的类型不符,会直接 panic。所以实际开发中,通常使用另一种类型断言形式c, ok := a.(Cat)
。如果类型不符,这种形式不会 panic,而是通过将第二个返回值置为 false 来表明这种情况。
但是一般情况下在运行时我们不会去推断这个空接口内部是什么值,我们希望有一种方法可以帮助我们做这种工作,例如很多时候,传递给这些空接口(interface{}
)的数据并不是基本类型,也可能是结构体。我们需要在不知道其具体类型或字段值的情况下,对这些数据执行某些操作。在这种情况下,如果我们想对结构体进行各种操作,例如:
- 解析其中的数据以查询数据库。
- 或根据其结构生成数据库的模式
我们就需要在运行时获取结构体中字段的类型信息以及字段数量,此时reflect
包就发挥作用了。
回头看我之前给出的TypeOf
和ValueOf
的函数定义,我们可以知道传入的参数是先赋给了空接口,空接口获取其底层对象和类型+接口方法集分别赋值给(type, value),然后通过其底层数据和静态类型进行相应操作。
下面来讲一讲使用反射的缺点。其实和C++差不多,都是基于RTTI来获取动态类型,每次获取时都会访问底层的实际数据和类型,访问时需要从 interface{}
中解析真实类型信息,需要一次指针的间接访问,扰乱缓存,在内存中找到底层数据还要进行元数据查询,还说说空接口的开销,上面说过,一个空接口实际上就是封装了一个指向类型信息的指针和一个指向实际数据的指针。空接口内存占有两个指针,而且每次使用时都需要间接访问,多一次查询开销而且会扰乱缓存。
这篇文章的内容就到这里,如果还想了解go语言反射的应用可以参考这篇文章。