关于golang:Go语言反射获取结构体字段的过程

64次阅读

共计 4270 个字符,预计需要花费 11 分钟才能阅读完成。

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

前言

哈喽,大家好,我是 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)
}
// 运行后果
0xc00000c060
0xc00000c060

从运行后果咱们能够验证了构造体变量 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 field
type 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 设计

正文完
 0