当前位置: 首页 > 科技观察

反射如何获取结构成员信息?

时间:2023-03-15 09:07:42 科技观察

本文转载自微信公众号《Golang梦工厂》,作者:AsongGo。转载本文请联系Golang梦工厂公众号。前言大家好,我是asong。今天这篇文章的目的是回答一个读者的问题。涉及到的知识点是反射和结构内存布局。我们先来看看读者的问题:我们通过两个问题来解开他的疑惑:结构体是如何存储在内存中的?获取结构体成员信息的反射过程结构体是如何存储的?结构体占用一块连续的内存,结构体变量的大小由结构体中的字段决定,结构体变量的地址等于结构体第一个字段的首地址。示例:typeUserstruct{NamestringAgeuint64Genderbool//true:malefalse:female}funcmain(){u:=User{Name:"asong",Age:18,Gender:false,}fmt.Printf("%p\n",&u)fmt.Printf("%p\n",&u.Name)}//运行结果0xc00000c0600xc00000c060从运行结果我们可以验证结构体变量u的存储地址就是字段Name的首地址。结构体的内存布局其实就是分配一块连续的内存。分配在栈上还是堆上,取决于编译器的逃逸分析。分配内存时还应考虑结构的内存对齐。对齐的作用及原因:CPU访问内存时,并不是逐字节访问,而是以字长为单位访问。比如对于32位的CPU,字长是4字节,那么CPU访问内存单元也是4字节。这种设计可以减少CPU访问内存的次数,提高CPU访问内存的吞吐量。假设我们需要读取8个字节的数据,如果一次读取4个字节,只需要读取2次即可。内存对齐也适用于变量的原子操作。每个内存访问都是原子的。如果变量的大小不超过字长,那么内存对齐后,对该变量的访问是原子的。这个特性在并发上下文中是至关重要的。C语言的内存对齐规则和Go语言一样,所以C语言的对齐规则也适用于Go:(offset)为0,每个成员相对于首地址的偏移量该结构是成员大小和有效对齐值中较小者的整数倍。如有必要,编译器将在成员之间添加填充字节。除了结构成员的对齐,结构本身也需要对齐。结构的长度必须是编译器默认的对齐长度和成员中最长类型的最小数据大小的倍数。根据这个规律,我们来分析一下上面例子中的结构体User。这里我用的是mac,所以是64位的CPU。编译器默认的对齐参数为8,String、uint64、bool的对齐值分别为8、8、1。按照第一条规则分析:第一个字段类型为string,对齐值为8,大小为16,所以在内存布局中放在最前面。第二个字段的类型是uin64,对齐值为8,大小为8,所以它的内存偏移值必须是8的倍数,因为第一个字段Name占用16位,所以直接从16开始,不填充。第三个字段的类型是bool,对齐值为1,大小为1,所以它的内存偏移值必须是1的倍数,因为User的前两个字段已经排成24位了,所以接下来offset正好是24。接下来我们在分析第二条规则:根据第一条内存对齐规则分析后,内存长度已经是25字节,我们开始使用第二条规则进行对齐,默认对齐值为8,字段中最大的类型长度为16,所以可以得出结构体的对齐值为8。我们当前内存长度为25,不是8的倍数,所以需要补全,所以最后result为32,加了7位,编译器填充,一般为0值,也称为void。注:这里不详细描述内存对齐。想了解更多内存对齐可以看我之前的文章:去读源码知unsafe包Go语言反射获取结构体成员信息Go语言提供了一种机制更新和检查变量的值在运行时,调用变量的方法和变量的内部操作,但在编译时并不知道这些变量的具体类型。这种机制称为反射。Go语言提供了reflect包来访问程序的反射信息。我们可以通过调用reflect.TypeOf()来获取反射对象信息。如果它的类型是结构体,那么我们可以通过反射值对象reflect.Type的NumField和Field方法获取结构体成员的详细信息。先看一个例子:typeUserstruct{NamestringAgeuint64Genderbool//true:malefalse:female}funcmain(){u:=User{Name:"asong",Age:18,Gender:false,}getType:=reflect.TypeOf(u)fori:=0;i=len(t.fields){panic("reflect:Fieldindexoutofbounds")}//获取前面structType中fields字段的值p:=&t.fields[i]//转为StructFiled结构f.Type=toType(p.typ)f.Name=p.name.name()//判断是否为匿名结构f.Anonymous=p.embedded()if!p.name.isExported(){f.PkgPath=t.pkgPath.name()}iftag:=p.name.tag();tag!=""{f.Tag=StructTag(tag)}//获取字段的偏移量f.Offset=p.offset()//获取索引值f.Index=[]int{i}return}返回StructField结构如下://AStructFielddescribesasasasinglefieldinstruct.typeStructFieldstruct{Namestring//字段名PkgPathstring//字段路径TypeType//字段反射类型对象TagStructTag//fieldstructureLabelOffsetuintptr//字段在结构体中的相对偏移量Index[]int//Type.FieldByIndex中返回的索引值Anonymousbool//是否为匿名字段}这里整个反映和获取结构体成员信息的过程应该很清楚好吗~。**总结:**由于Go语言中的所有类型都实现了空接口,所以根据这个特点可以得到数据类型和数据存放的地址。对于结构体类型,转换为structType类型,最后转换为StructFieldStructure获取所有结构体信息。小结本文无意详细讲解Go语言反射的原理和过程,只是简单介绍一下通过反射获取结构体成员信息的过程。后续会更新更多关于反射知识的讲解,敬请期待~。