文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-03 00:44

关注

前言

哈喽,大家好,我是asong,今天这篇文章的目的主要是解答一位读者的疑问,涉及知识点是反射和结构体内存布局。我们先看一下读者的问题:

我们通过两个问题来解决他的疑惑:

  1. 结构体在内存中是如何存储的
  2. 反射获取结构体成员信息的过程

结构体是如何存储的

结构体是占用一块连续的内存,一个结构体变量的大小是由结构体中的字段决定的,结构体变量的地址等于结构体第一个字段的首地址。示例:

  1. type User struct { 
  2.  Name string 
  3.  Age uint64 
  4.  Gender bool // true:男 false: 女 
  5.  
  6. func main(){ 
  7.  u := User
  8.    Name"asong"
  9.    Age: 18, 
  10.    Gender: false
  11.   } 
  12.  fmt.Printf("%p\n",&u) 
  13.  fmt.Printf("%p\n",&u.Name
  14. // 运行结果 
  15. 0xc00000c060 
  16. 0xc00000c060 

从运行结果我们可以验证了结构体变量u的存放地址就是字段Name的首地址。

结构体的内存布局其实就是分配一段连续的内存,具体是在栈上分配还是堆上分配取决于编译器的逃逸分析,结构体在内存分配时还要考虑到内存对齐。

对齐的作用和原因:CPU访问内存时,并不是逐个字节访问,而是以字长(word size)单位访问。比如32位的CPU,字长为4字节,那么CPU访问内存的单位也是4字节。这样设计可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量。假设我们需要读取8个字节的数据,一次读取4个字节那么就只需读取2次就可以。内存对齐对实现变量的原子性操作也是有好处的,每次内存访问都是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。

C语言的内存对齐规则与Go语言一样,所以C语言的对齐规则对Go同样适用:

根据这个规则我们来分析一下上面示例的结构体User,这里我使用的mac,所以是64位CPU,编译器默认对齐参数是8,String、uint64、bool的对齐值分别是8、8、1,根据第一条规则分析:

接下来我们在分析第二个规则:

注意:这里对内存对齐没有说的很细,想要更深了解内存对齐可以看我之前的一篇文章:Go看源码必会知识之unsafe包

Go语言反射获取结构体成员信息

Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。Go语言提供了 reflect 包来访问程序的反射信息。

我们可以通过调用reflect.TypeOf()获得反射对象信息,如果他的类型是结构体,接着可以通过反射值对象reflect.Type的NumField和Field方法获取结构体成员的详细信息,先看一个例子:

  1. type User struct { 
  2.  Name string 
  3.  Age uint64 
  4.  Gender bool // true:男 false: 女 
  5.  
  6.  
  7. func main()  { 
  8.  u := User
  9.   Name"asong"
  10.   Age: 18, 
  11.   Gender: false
  12.  } 
  13.  getType := reflect.TypeOf(u) 
  14.  for i:=0; i < getType.NumField(); i++{ 
  15.   fieldType := getType.Field(i) 
  16.   // 输出成员名 
  17.   fmt.Printf("name: %v \n", fieldType.Name
  18.  } 
  19. // 运行结果 
  20. nameName  
  21. name: Age  
  22. name: Gender  

接下来我们就一起来看一看Go语言是如何通过反射来获取结构体成员信息的。

首先我们来看一看reflect.TypeOf()方法是如何获取到类型的:

  1. func TypeOf(i interface{}) Type { 
  2.  eface := *(*emptyInterface)(unsafe.Pointer(&i)) 
  3.  return toType(eface.typ) 

我们知道在Go语言中任何类型都可以转成interface{}类型,当向接口变量赋于一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的。

一个空接口结构如下:

  1. type eface struct { 
  2.     _type *_type 
  3.     data  unsafe.Pointer 

_type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值,Go 语言里所有的类型都 实现了 空接口。

所以在TypeOf方法中,我们就是通过读取_type字段获取到类型。

现在我们已经知道他是怎么获取到具体的类型了,接下来我们就来看一看NumField()方法是怎么获取到字段的。

  1. func (t *rtype) Kind() Kind { return Kind(t.kind & kindMask) } 
  2. func (t *rtype) NumField() int { 
  3.  if t.Kind() != Struct { 
  4.   panic("reflect: NumField of non-struct type " + t.String()) 
  5.  } 
  6.  tt := (*structType)(unsafe.Pointer(t)) 
  7.  return len(tt.fields) 

因为只有struct类型才可以调用,所以在NumFiled()方法中做了类型检查,如果不是struct类型则直接发生panic,然后会rtype类型强制转换成structType,最后返回结构体成员字段的数量。

  1. // structType represents a struct type. 
  2. type structType struct { 
  3.  rtype 
  4.  pkgPath name 
  5.  fields  []structField // sorted by offset 
  6. // Struct field 
  7. type structField struct { 
  8.  name        name    // name is always non-empty 
  9.  typ         *rtype  // type of field 
  10.  offsetEmbed uintptr // byte offset of field<<1 | isEmbedded 

调用Field()方法会根据索引返回对应的结构体字段的信息,当值不是结构体或索引超界时发生panic。

  1. func (t *rtype) Field(i int) StructField { 
  2.   // 类型检查 
  3.  if t.Kind() != Struct { 
  4.   panic("reflect: Field of non-struct type " + t.String()) 
  5.  } 
  6.   // 强制转换成structType 类型 
  7.  tt := (*structType)(unsafe.Pointer(t)) 
  8.  return tt.Field(i) 
  9. // Field returns the i'th struct field. 
  10. func (t *structType) Field(i int) (f StructField) { 
  11.   // 溢出检查 
  12.  if i < 0 || i >= len(t.fields) { 
  13.   panic("reflect: Field index out of bounds"
  14.  } 
  15.  // 获取之前structType中fields字段的值 
  16.  p := &t.fields[i] 
  17.   // 转换成StructFiled结构体 
  18.  f.Type = toType(p.typ) 
  19.  f.Name = p.name.name() 
  20.   // 判断是否是匿名结构体 
  21.  f.Anonymous = p.embedded() 
  22.  if !p.name.isExported() { 
  23.   f.PkgPath = t.pkgPath.name() 
  24.  } 
  25.  if tag := p.name.tag(); tag != "" { 
  26.   f.Tag = StructTag(tag) 
  27.  } 
  28.   // 获取字段的偏移量 
  29.  f.Offset = p.offset() 
  30.   // 获取索引值 
  31.  f.Index = []int{i} 
  32.  return 

返回StructField结构如下:

  1. // A StructField describes a single field in a struct. 
  2. type StructField struct { 
  3.    Name string // 字段名 
  4.    PkgPath string // 字段路径 
  5.    Type      Type      // 字段反射类型对象 
  6.    Tag       StructTag // 字段的结构体标签 
  7.    Offset    uintptr   // 字段在结构体中的相对偏移 
  8.    Index     []int     // Type.FieldByIndex中的返回的索引值 
  9.    Anonymous bool      // 是否为匿名字段 

到这里整个反射获取结构体成员信息的过程应该很明朗了吧~。

**小结:**因为Go 语言里所有的类型都 实现了 空接口,所以可以根据这个特性获取到数据类型以及存放数据的地址,对于结构体类型,将其转换为structType类型,最后转换成StructField结构获取所有结构体信息。

总结

 

本文没想详细展开讲解Go语言反射的原理和过程,只是简单介绍了一下反射获取到结构体成员信息的过程,更多关于反射知识的讲解会在后面持续更新,敬请期待~。

 

来源: Golang梦工厂内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯