Go语言--反射
了解Go反射基础的之前,我们先了解下Go语言中的变量组成规则,在Go语言中变量的组成分为两部分:
1、变量类型信息:也就是预先定义好的元信息
2、变量值信息:指的是程序运行过程可动态变化的部分
在Go语言中,通常我们变量的定义方式为 var a int = 1,有声明变量类型与值,此类场景我们一般称为正射。
那么什么是反射呢?
在计算机科学领域,反射是指一类应用,他们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义–反射就是程序在运行的时候能够“观察”并且修改自己的行为,而在Go语言中,其实就是编译时是interface{}接口类型的变量,不知道其具体的类型,需要在运行中反射获取其类型,更新检查他们的值与执行他们的方法,当然这里要注意一点,在Go中,interface{}接口类型变量可以存储任意类型的变量,而Go中的反射高度依赖interface{}这个万能的容器进行功能实现。也就是在拿到一个结构体时,哪怕不知道该结构体是啥,也不知道对象有什么方法,当时依据反射的功能,我们依旧可以操作该结构体。
例如示例:
Go语言中反射的实现(reflect包)
在Go语言中,提供了reflect包实现反射功能,在其反射机制中,任何一个接口值都由是一个具体类型和一个具体类型的值两部分组成的。在go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type 和 reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。
举例:我们可以通过反射动态的获取当前接收的变量的类型与值
Go反射的三大法则
第一法则:将interface{}变量转换成反射对象
转换过程:1、从基本类型到接口类型的类型转换(从基本类型到接口类型的类型转换(隐式转换),分两种类型,包含方法的interface,由runtime.iface实现 不包含方法的interface,由runtime.eface实现)
2、从接口类型到反射对象的转换
举例:
第二法则:从反射对象获取interface{}变量
转换过程:1、反射对象转换成接口类型
2、通过显示类型转换变成原始类型
举例:
第三法则:如果要修改“反射值对象”其值必须是“可写的”
也就是可以将指定类型的变量转换为接口类型,接口类型转换为反射类型,反射类型转换为接口类型,接口类型转换为指定类型。如果可以更新反射对象,就可以更新对应的变量。
但通过反射修改原对象变量值时需要注意一点,就是Go语言的函数调用都是传值的,所以我们得到的反射对象跟最开始的变量是没有任何关系的,举例:
上边的程序执行时会出现panic报错,这是因为上面所说的,反射对象跟最开始的变量是没有任何关系的。
我们可以查看当前变量是否处于可写状态来分析:
反射对象 v 是不可写的,因为它是指针类型。需要通过解引用来修改它指向的值。而在Go reflect包中可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”(注意点:在Go语言中,&符号的意思是对变量取地址 *符号的意思是对指针取值)
所以正确的写法应该是:
结构体反射
通过上述例子,我们了解到了常规类型的反射概念与操作方法,那么在Go中不可或缺的结构体类型变量要怎么通过反射去操作呢?
在Go语言中,任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型时结构体,可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。
举例:
当然,我们也可以直接获取结构体的对象成员tags等信息
func TestStruct(a interface{}) {typ := reflect.TypeOf(a)tag := typ.Elem().Field(0).Tag.Get("json")fmt.Printf("Tag:%s\n", tag)}
而且当你的结构体中成员含有函数时,也可以通过反射进行call回调,不过需要注意一点,Call的参数中需要传入的是切片
反射性能分析
结构体成员赋值性能对比
解读:1、原生结构体成员赋值,每一步操作耗时0.28纳秒
2、使用反射的结构体成员赋值,操作耗时2.47纳秒,比原生赋值多消耗8倍的性能
3、反射查找结构体成员并赋值,耗时59.88纳秒,消耗时间是原生的213倍之多
函数调用性能对比
解读:1、原生函数调用,性能与原生访问结构体成员接近
2、反射函数调用,耗时110.8纳秒,消耗时间是原生的422倍
反射优缺点整理:
优点:1、可以在一定程序上避免硬编码,提高灵活性和通用性,它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:1、反射的概念和语法比较抽象,与反射相关的代码,经常是难以阅读的(Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的,所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic),会造成部分类型检查工作从编译期推迟到运行时,会使得一些隐藏的问题无法在编译期发现,提高的bug代码出现的几率;
2、反射出变量的类型需要额外的开销,对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。
反射性能低的原因:通过反射在获取或者修改值内容时,多了几次内存引用,肯定没有直接调用某个值来的迅速,这个是反射的固定性能损失,还有另一方面的性能损失在于,结构体类型字段比较多时,要进行遍历匹配才能获取对应的内容。
总结
反射是一把双刃剑,善用反射能给我们的程序提供良好的灵活性等优势,并不是因为它存在缺点我们就完全弃用。
参考文档
Go反射定律:https://go.dev/blog/laws-of-reflection
Go语言设计与实现:https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/