反射是如何獲取結構體成員信息的?
本文轉載自微信公眾號「Golang夢工廠」,作者AsongGo。轉載本文請聯系Golang夢工廠公眾號。
前言
哈嘍,大家好,我是asong,今天這篇文章的目的主要是解答一位讀者的疑問,涉及知識點是反射和結構體內存布局。我們先看一下讀者的問題:
我們通過兩個問題來解決他的疑惑:
- 結構體在內存中是如何存儲的
- 反射獲取結構體成員信息的過程
結構體是如何存儲的
結構體是占用一塊連續的內存,一個結構體變量的大小是由結構體中的字段決定的,結構體變量的地址等于結構體第一個字段的首地址。示例:
- 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,所以是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位,所以下一個偏移量正好是24。
接下來我們在分析第二個規則:
- 根據第一條內存對齊規則分析后,內存長度已經為25字節了,我們開始使用第2條規則進行對齊,默認對齊值是8,字段中最大類型的長度是16,所以可以得出該結構體的對齊值是8,我們目前的內存長度是25,不是8的倍數,所以需要補全,所以最終的結果是32,補了7位,由編譯器進行填充,一般為0值,也稱之為空洞。
注意:這里對內存對齊沒有說的很細,想要更深了解內存對齊可以看我之前的一篇文章:Go看源碼必會知識之unsafe包
Go語言反射獲取結構體成員信息
Go語言提供了一種機制在運行時更新和檢查變量的值、調用變量的方法和變量的內在操作,但是在編譯時并不知道這些變量的具體類型,這種機制被稱為反射。Go語言提供了 reflect 包來訪問程序的反射信息。
我們可以通過調用reflect.TypeOf()獲得反射對象信息,如果他的類型是結構體,接著可以通過反射值對象reflect.Type的NumField和Field方法獲取結構體成員的詳細信息,先看一個例子:
- 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語言反射的原理和過程,只是簡單介紹了一下反射獲取到結構體成員信息的過程,更多關于反射知識的講解會在后面持續更新,敬請期待~。