本文转载自微信公众号《我的大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。今天是2020年的最后一天,让我们继续快乐地学习吧:)。在所有的语言中,反射的功能基本上是不可或缺的模块。虽然“反思”这个词根深蒂固,但更多的是关于为什么。反射到底是什么,它基于什么规则?今天我们就通过这篇文章来一一揭秘。以Go语言为例,我们就明白了什么是反射,底层又是如何实现的。什么是反射在计算机科学中,反射是指计算机程序在运行时访问、检测和修改自身状态或行为的能力。比喻地说,反射是程序在运行时“观察”和修改自身行为的能力(来自维基百科)。简单地说,应用程序可以在运行时观察到变量的值并可以对其进行修改。最常见的反射标准库示例如下:import("fmt""reflect")funcmain(){rv:=[]interface{}{"hi",42,func(){}}for_,v:=rangerv{switchv:=reflect.ValueOf(v);v.Kind(){casereflect.String:fmt.Println(v.String())casereflect.Int,reflect.Int8,reflect.Int16,reflect.Int32,reflect.Int64:fmt.Println(v.Int())default:fmt.Printf("unhandledkind%s",v.Kind())}}}输出结果:hi42unhandledkindfunc主要声明rv变量和变量类型在program是interface{},里面包含了3种不同类型的值,分别是字符串、数字和闭包。在使用interface{}的时候,通常不知道参与者具体的基本类型是什么,那么我们就用interface{}类型来做一个伪“泛型”。这时候,就会出现一个新的问题。既然入参是interface{},那出参呢?Go语言是强类型语言,入参是interface{},出参一定跑不掉,所以一定要分开如果没有开启类型判断,这时候就用到了反射,即反映标准库。(type)的类型断言是在反射之??后进行的。这是我们写程序时最常遇到的反射使用场景之一。在Go的reflect标准库中,核心是reflect.Type和reflect.Value类型。反射中使用的方法都是围绕这两个展开的,方法的主要含义如下:TypeOf方法:用于提取输入参数值的类型信息。ValueOf方法:用于提取存储变量的值信息。reflect.TypeOf演示程序:funcmain(){blog:=Blog{"Friedfish"}typeof:=reflect.TypeOf(blog)fmt.Println(typeof.String())}输出结果:main.Blog从输出结果,可以得出reflect.TypeOf成功解析出blog变量的类型是main.Blog,也就是连包都知道了。从人类识别的角度来看,这似乎很正常,但程序却并非如此。他怎么知道“他”在哪个包裹下?一起来看源码:funcTypeOf(iinterface{})Type{eface:=*(*emptyInterface)(unsafe.Pointer(&i))returntoType(eface.typ)}从源码来看,主要是TypeOf方法涉及三个操作,分别是:使用unsafe.Pointer方法获取任意类型的可寻址指针值。强制接口类型转换为空接口类型。调用toType方法转换为可以对外使用的Type。其中,emptyInterface结构中的rtype类型信息量最大:typertypestruct{sizeuintptrptrdatauintptrhashuint32tflagtflagalignuint8fieldAlignuint8kinduint8equalfunc(unsafe.Pointer,unsafe.Pointer)boolgcdata*bytestrnameOffptrToThistypeOff}使用最重要的类型是rtype类型,它实现了所有Interface方法,所以他可以直接作为Type类型返回。而Type本质上是一个接口实现,包含了获取一个类型所需要的所有方法:typeTypeinterface{//适用于所有类型//返回内存对齐后该类型占用的字节数Align()int//只作用于strcut类型//返回该类型内存对齐占用的字节数FieldAlign()int//返回该类型方法集中的第i个方法Method(int)Method//根据获取对应的方法集themethodnameMethodMethodByName(string)(Method,bool)//返回该类型的方法集中导出的方法数。NumMethod()int//返回这个类型的名字Name()string...}建议先粗略的过一遍,了解一下都有哪些方法,然后看就行了。主要思路是自己脑补一个索引,方便以后在pkg.go.dev上快速查询。reflect.ValueOf演示程序:funcmain(){varxfloat64=3.4fmt.Println("value:",reflect.ValueOf(x))}输出结果:value:3.4从输出结果可以知道,通过reflect.ValueOf变量x的值为3.4。与reflect.TypeOf形成匹配,一个负责获取类型,一个负责获取值。那么reflect.ValueOf是如何获取值的呢?核心源码如下:funcValueOf(iinterface{})Value{ifi==nil{returnValue{}}escapes(i)returnunpackEface(i)}funcunpackEface(iinterface{})Value{e:=(*emptyInterface)(unsafe.Pointer(&i))t:=e.typift==nil{returnValue{}}f:=flag(t.Kind())ififaceIndir(t){f|=flagIndir}returnValue{t,e.word,f}}从源码来看,ValueOf方法主要有以下操作:调用escape让变量i逃逸到堆中。将变量i转换为类型emptyInterface。将需要的信息(包括值的具体类型和指针)组装成reflect.Value类型后返回。当类型转换调用reflect进行一系列反射行为时,Go什么时候进行类型转换呢?毕竟我们传入的是float64,参数等函数都是inetrface类型。检查编译如下:$gotoolcompile-Smain.go...0x005800088($GOROOT/src/reflect/value.go:2817)LEAQtype.float64(SB),CX0x005f00095($GOROOT/src/reflect/value.go:第2817章(SB),$00x006d00109($GOROOT/src/reflect/value.go:2817)JNE3570x007300115($GOROOT/src/reflect/value.go:2817)MOVQAX,reflect.dummy+16(SB)0x007a00122($GOROOT/src/reflect/value.go:2348)PCDATA$0,$-10x007a00122($GOROOT/src/reflect/value.go:2348)MOVQCX,reflect.i+64(SP)0x007f00127($GOROOT/src/reflect/value.go:2348)MOVQAX,reflect.i+72(SP)...显然,Go语言会在编译阶段完成解析和类型转换。这样,reflect实际使用的是接口类型。reflect.Set演示程序:funcmain(){i:=2.33v:=reflect.ValueOf(&i)v.Elem().SetFloat(6.66)log.Println("value:",i)}输出结果:value:6.66从输出结果可以知道,调用reflect.ValueOf方法后,我们使用SetFloat方法改变了值。核心方法之一是Setter相关的方法。我们可以看看它的源码是如何实现的:func(vValue)Set(xValue){v.mustBeAssignable()x.mustBeExported()//donotletunexportedxleakvartargetunsafe.Pointerifv.kind()==Interface{target=v.ptr}x=x.assignTo("reflect.Set",v.typ,target)ifx.flag&flagIndir!=0{typedmemmove(v.typ,v.ptr,x.ptr)}else{*(*unsafe.Pointer)(v.ptr)=x.ptr}}检查反射对象及其字段是否可以设置。检查反射对象及其字段是否被导出(暴露给外界)。调用assignTo方法创建一个新的反射对象并覆盖原来的反射对象。根据assignTo方法返回的指针值修改当前反射对象的指针值。简单的说就是检查是否可以设置,然后新建对象,最后修改。这是一个非常标准的分配过程。反射三定律Go语言中的反射,说到底就是实现三大定律:反射从接口值到反射对象。反射从反射对象到接口值。要修改反射对象,该值必须是可设置的。我们将介绍和解释三个核心法则,以了解Go反射中各种方法实现的概念。第一定律反射第一定律是:“反射可以从接口值中得到反射对象”。示例代码:funcmain(){varxfloat64=3.4fmt.Println("type:",reflect.TypeOf(x))}输出结果:type:float64有些读者可能会疑惑,我在代码中明确传入了变量x,他的类型是float64。如何从接口值中获取反射对象。事实上,情况并非如此。虽然我们在代码中传入的变量的基本类型是float64,但是reflect.TypeOf方法的入参是interface{},在Go语言中本质上是类型转换。这部分将在后面进一步解释。第二定律反射第二定律是:“可以从被反射的对象中获取接口值(interface)”。它是与第一定律相反的定律,可以相互补充。示例代码:funcmain(){vo:=reflect.ValueOf(3.4)vf:=vo.Interface().(float64)log.Println("value:",vf)}输出结果:value:3.4可见在示例代码中,变量vo已经是一个反射对象,然后我们可以使用它提供的Interface方法获取接口值(interface),最后将其强制转换回我们原来的变量类型。第三定律反射第三定律是:“要修改一个反射对象,其值必须是可修改的”。第三定律与第一、第二看似没有直接关系,但却是必不可少的,因为在工程实践中,反射的第一目的是获取值和类型,第二是能够修改它们。价值。否则倒影只能看不能动,会导致倒影很鸡肋。例如:应用中的配置热更新,必然会涉及到配置项相关的变量变化,大部分都会使用反射来改变初始值。示例代码:funcmain(){i:=2.33v:=reflect.ValueOf(&i)v.Elem().SetFloat(6.66)log.Println("value:",i)}输出结果:value:6.66单从结果,变量i的值确实从2.33变成了6.66,看起来很完美。但是光看代码,好像有些“问题”。如何设置反射值好“麻烦”:为什么一定要传入变量i的指针引用?为什么变量v在设置之前需要Elem?叛逆的Gophper说我不会这样设置行不行?会不会有什么问题:funcmain(){i:=2.33reflect.ValueOf(i).SetFloat(6.66)log.Println("value:",i)}报错:panic:reflect:reflect.Value.SetFloatusingunaddressablevaluegoroutine1[运行]:reflect.flag.mustBeAssignableSlow(0x8e)/usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:259+0x138reflect.flag.mustBeAssignable(...)/usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:246reflect.Value.SetFloat(0x10b2980,0xc00001a0b0,0x8e,0x401aa3d70a3d70a4)/usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:1609+0x37main.main()/Users/eddycjy/go-application/awesomeProject/main.go:10+0xc5根据上面的提示,由于使用了“useunaddressablevalue”,所以示例程序无法正常运行.而这是reflect标准库本身防范的硬性要求。之所以会这样,是因为Go语言中函数调用的传递是值拷贝,所以如果不传递指针引用,单纯的按值传递,那么肯定不可能改变反射对象的源值。因此,Go标准库对其进行逻辑判断,避免出现问题。因此,当我们要改变反射对象的source值时,必须主动传入对应变量的指针引用,并调用reflect标准库的Elem方法获取该指针指向的source变量,而最后调用Set相关方法进行设置。小结通过这篇文章,我们了解并了解了Go反射是如何使用的,它基于什么规律。另外,稍微注意一下,不难发现Go的反射是基于接口实现的。此外,Go语言中的很多运行时函数都是基于接口实现的。一般来说,Go的反射是围绕这三者进行的,即Type、Value和Interface。三者相辅相成,反射在本质上与Interface直接相关。我们也会在后续的文章中讨论Interface的内容。分析。
