Golang底层原理剖析之反射reflect

简介: Golang底层原理剖析之反射reflect

前言



反射的作用,就是把类型元数据暴露给用户使用,其实在了解了类型系统和接口以后,反射所做的事情就没什么神奇的了。

我们已经介绍过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,这里这一次修改的就是它

通过反射修改变量值的问题有点绕,不过只要理解传参值拷贝的语义,以及通过反射修改变量值要作用到原变量身上才有意义,理解起来就会相对容易了

目录
相关文章
|
1月前
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
122 29
|
1月前
|
负载均衡 算法 Go
GoLang协程Goroutiney原理与GMP模型详解
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
|
4月前
|
存储 关系型数据库 Go
SOLID原理:用Golang的例子来解释
SOLID原理:用Golang的例子来解释
|
4月前
|
算法 NoSQL 关系型数据库
熔断原理与实现Golang版
熔断原理与实现Golang版
|
4月前
|
存储 人工智能 Go
golang 反射基本原理及用法
golang 反射基本原理及用法
36 0
|
7月前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
472 1
|
7月前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
103 2
|
7月前
|
JSON 编译器 Go
Golang深入浅出之-结构体标签(Tags):JSON序列化与反射应用
【4月更文挑战第22天】Go语言结构体标签用于添加元信息,常用于JSON序列化和ORM框架。本文聚焦JSON序列化和反射应用,讨论了如何使用`json`标签处理敏感字段、实现`omitempty`、自定义字段名和嵌套结构体。同时,通过反射访问标签信息,但应注意反射可能带来的性能问题。正确使用结构体标签能提升代码质量和安全性。
337 0
|
7月前
|
Java 编译器 Go
Golang底层原理剖析之内存逃逸
Golang底层原理剖析之内存逃逸
54 0
|
7月前
|
存储 算法 Java
Golang底层原理剖析之多路select、channel数据结构和阻塞与非阻塞
Golang底层原理剖析之多路select、channel数据结构和阻塞与非阻塞
93 0