原文链接:读者发问:反射是如何获取构造体成员信息的?

前言

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

咱们通过两个问题来解决他的纳闷:

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

构造体是如何存储的

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

type User struct {    Name string    Age uint64    Gender bool // true:男 false: 女}func main(){    u := User{            Name: "asong",            Age: 18,            Gender: false,        }    fmt.Printf("%p\n",&u)    fmt.Printf("%p\n",&u.Name)}// 运行后果0xc00000c0600xc00000c060

从运行后果咱们能够验证了构造体变量u的寄存地址就是字段Name的首地址。

构造体的内存布局其实就是调配一段间断的内存,具体是在栈上调配还是堆上调配取决于编译器的逃逸剖析,构造体在内存调配时还要思考到内存对齐。

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

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

  • 对于构造的各个成员,第一个成员位于偏移为0的地位,构造体第一个成员的偏移量(offset)为0,当前每个成员绝对于构造体首地址的 offset 都是该成员大小与无效对齐值中较小那个的整数倍,如有须要编译器会在成员之间加上填充字节。
  • 除了构造成员须要对齐,构造自身也须要对齐,构造的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。

依据这个规定咱们来剖析一下下面示例的构造体User,这里我应用的mac,所以是64CPU,编译器默认对齐参数是8Stringuint64bool的对齐值别离是881,依据第一条规定剖析:

  • 第一个字段类型是string,对齐值是8,大小为16,所以放在内存布局中的第一位。
  • 第二个字段类型是uin64,对齐值是8,大小为8,所以他的内存偏移值必须是8的倍数,因为第一个字段Name占有16位,所以间接从16开始不要补位。
  • 第三个字段类型是bool,对齐值是1,大小为1,所以他的内存偏移值必须是1的倍数,因为User的前两个字段曾经排到了24位,所以下一个偏移量正好是24

接下来咱们在剖析第二个规定:

  • 依据第一条内存对齐规定剖析后,内存长度曾经为25字节了,咱们开始应用第2条规定进行对齐,默认对齐值是8,字段中最大类型的长度是16,所以能够得出该构造体的对齐值是8,咱们目前的内存长度是25,不是8的倍数,所以须要补全,所以最终的后果是32,补了7位,由编译器进行填充,个别为0值,也称之为空洞。

留神:这里对内存对齐没有说的很细,想要更深理解内存对齐能够看我之前的一篇文章:Go看源码必会常识之unsafe包

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

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

咱们能够通过调用reflect.TypeOf()取得反射对象信息,如果他的类型是构造体,接着能够通过反射值对象reflect.TypeNumFieldField办法获取构造体成员的详细信息,先看一个例子:

type User struct {    Name string    Age uint64    Gender bool // true:男 false: 女}func main()  {    u := User{        Name: "asong",        Age: 18,        Gender: false,    }    getType := reflect.TypeOf(u)    for i:=0; i < getType.NumField(); i++{        fieldType := getType.Field(i)        // 输入成员名        fmt.Printf("name: %v \n", fieldType.Name)    }}// 运行后果name: Name name: Age name: Gender 

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

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

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

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

一个空接口构造如下:

type eface struct {    _type *_type    data  unsafe.Pointer}

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

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

当初咱们曾经晓得他是怎么获取到具体的类型了,接下来咱们就来看一看NumField()办法是怎么获取到字段的。

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

因为只有struct类型才能够调用,所以在NumFiled()办法中做了类型查看,如果不是struct类型则间接产生panic,而后会rtype类型强制转换成structType,最初返回构造体成员字段的数量。

// structType represents a struct type.type structType struct {    rtype    pkgPath name    fields  []structField // sorted by offset}// Struct fieldtype structField struct {    name        name    // name is always non-empty    typ         *rtype  // type of field    offsetEmbed uintptr // byte offset of field<<1 | isEmbedded}

调用Field()办法会依据索引返回对应的构造体字段的信息,当值不是构造体或索引超界时产生panic

func (t *rtype) Field(i int) StructField {  // 类型查看    if t.Kind() != Struct {        panic("reflect: Field of non-struct type " + t.String())    }  // 强制转换成structType 类型    tt := (*structType)(unsafe.Pointer(t))    return tt.Field(i)}// Field returns the i'th struct field.func (t *structType) Field(i int) (f StructField) {  // 溢出检查    if i < 0 || i >= len(t.fields) {        panic("reflect: Field index out of bounds")    }    // 获取之前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()    }    if tag := p.name.tag(); tag != "" {        f.Tag = StructTag(tag)    }  // 获取字段的偏移量    f.Offset = p.offset()  // 获取索引值    f.Index = []int{i}    return}

返回StructField构造如下:

// A StructField describes a single field in a struct.type StructField struct {   Name string // 字段名   PkgPath string // 字段门路   Type      Type      // 字段反射类型对象   Tag       StructTag // 字段的构造体标签   Offset    uintptr   // 字段在构造体中的绝对偏移   Index     []int     // Type.FieldByIndex中的返回的索引值   Anonymous bool      // 是否为匿名字段}

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

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

总结

本文没想具体开展解说Go语言反射的原理和过程,只是简略介绍了一下反射获取到构造体成员信息的过程,更多对于反射常识的解说会在前面继续更新,敬请期待~。

欢送关注公众号:Golang梦工厂

举荐往期文章:

  • 学习channel设计:从入门到放弃
  • 编程模式之Go如何实现装璜器
  • Go语言中new和make你应用哪个来分配内存?
  • 源码分析panic与recover,看不懂你打我好了!
  • 空构造体引发的大型打脸现场
  • 面试官:两个nil比拟后果是什么?
  • 面试官:你能用Go写段代码判断以后零碎的存储形式吗?
  • 赏析Singleflight设计