前言
反射的作用,就是把类型元数据暴露给用户使用,其实在了解了类型系统和接口以后,反射所做的事情就没什么神奇的了。
我们已经介绍过runtime包中,类型元数据以及空接口和非空接口的结构了,但是这些类型都是未导出的,所以reflect包中又定义了一套,这些类型定义在两个包中是保持一致的。
reflect.TypeOf
reflect包提供TypeOf函数,用于获取一个变量的类型信息。它接收一个空接口类型的参数,并返回一个reflect.Type接口类型的返回值。
reflect.Type是一个非空接口,提供了一系列方法,供用户获取类型各方面的信息例如对齐边界Align,方法Method,类型名称Name,包路径PkgPath,是否实现指定接口Implements,是否可比较Comparable等等等等方法
如果我们在eggo包中定义一个Eggo类型,在main包中使用这个类型,并且想通过反射,看看这个类型有多少个可导出的方法。
从函数调用栈来看,main函数栈帧中有两个局部变量,Eggo类型的a,reflect.Type类型的t,然后是返回值空间,最后是参数,Go语言中传参都是值拷贝,参数空间这里本应该拷贝a的值过来,但是不行。因为参数是空接口类型,它需要的是一个地址。
难道要拷贝a的地址过来?也不行,因为按照传参值拷贝的语义,被调用函数使用的应该是a的拷贝。也就是说,无论它对参数做什么样的修改,都不应该作用到原变量a的身上,如果我们直接拷贝a的地址过来,就不符合这样的语义了。
既然不能拷贝a,又不能拷贝a的地址,那该拷贝谁?实际上编译阶段会增加一个临时变量作为a的拷贝,然后在参数空间这里,使用这个临时变量的地址,现在TypeOf函数使用的就是a的拷贝,这样既符合传参值拷贝的语义,又满足了空接口类型的参数,只能接收地址的需求,所有参数为空接口类型的情况,都要像这样通过传递拷贝后变量的地址,来实现传值的语义
我们继续看TypeOf函数,接下来它会把这个runtime.eface类型的参数,转换成reflcet.emptyInterface类型,并赋给eface变量。
runtime.eface与reflcet.emptyInterface这两个类型的结构是一致的,转换以后方便reflect包操作内部元素
因为rtype类型实现了Type接口,所以TypeOf函数接下来要做的就是把eface.typ包装成reflect.Type类型的返回值,reflect.TypeOf的任务就完成了
还记得非空接口的结构吗,itab这里接口类型自然是reflect.Type,动态类型是rtype,data就等于eface.type,也就是反射变量的类型元数据的地址,而fun对应的方法,也不过是动态类型的类型元数据那里读取各种信息罢了
回到之前的例子,这个返回值长什么样?一个reflect.Type和*rtype组合对应的itab指针,一个Eggo类型元数据的地址。所以我们通过reflect.TypeOf拿到的,就是这样一个非空接口变量,然后把返回值赋值给局部变量t,接下来通过t调用这些方法,就会去Eggo类型元数据这里查找相关信息。
ok,这就是反射获取类型信息的方式
reflect.ValueOf
通过反射修改变量值,这就要用到reflect.Value类型的,这是一个结构体类型,第一个字段存储反射变量的类型元数据指针,第二个字段存储数据地址,第三个字段是一个位标识符,存储反射值的一些描述信息,例如是否为指针,是否为方法,是否只读等等,通过会用reflect.ValueOf来拿到一个reflect.Value
注意这个函数的参数也是是空接口类型,所以和reflect.TypeOf参数处理方式一样,除此之外,ValueOf函数会显示地把参数指向的变量逃逸到堆上
这里想通过反射,修改一个string类型的变量a,main函数栈帧中,有一个string类型的局部变量a,还有一个reflect.Value类型的局部变量v,同TypeOf一样的是,编译阶段会增加一个临时变量作为a的拷贝,同TypeOf不一样的是,这个临时变量,会被显示的逃逸到堆上,栈上只留它的地址,后面是调用reflect.ValueOf函数的返回值空间以及参数空间,参数这里data指向a的拷贝,_type指向string类型元数据,reflect.ValueOf的返回值这里,typ就等于参数的第一个字段,ptr就等于参数的第二个字段,再把flag处理好,reflect.ValueOf的任务就完成了。所以局部变量v就等于这个返回值。
接下来通过v调用SetString时,因为ptr指向a的拷贝而不是a,而修改这样一个用户都不知道的临时变量,没有任何意义,所以会发生panic,提醒我们这里用反射修改变量值是行不通的
若想修改成功,就要反射a的指针,这样ValueOf函数参数指向的变量就是a,所以main函数栈帧中,局部变量a逃逸到堆上,栈上只留一个地址,然后是局部变量v,返回值,和参数。参数这里,_type指向*string类型元数据,而data指向a,所以ValueOf的返回值就是这样的,然后它会赋值给局部变量v。
接下来调用v.Elem()方法,会拿到v.prt指向的变量a,并把它包装成reflect.Value类型的返回值。返回值中类型是string类型元数据,地址指向堆上的a。然后这个返回值被赋给v。
此时再通过v调用SetString方法时,方法接收者作为第一个参数,字符串新值作为第二个参数,v.prt指向a,这里这一次修改的就是它
通过反射修改变量值的问题有点绕,不过只要理解传参值拷贝的语义,以及通过反射修改变量值要作用到原变量身上才有意义,理解起来就会相对容易了