1. 程式人生 > >082-反射(結構體欄位遍歷)

082-反射(結構體欄位遍歷)

如何知道一個未知結構體包含哪些欄位呢?利用反射,可以很容易做到。

1. 遍歷結構體的 field 和 method

還記得 reflect.Type 介面吧,這個介面還包含這 4 個方法:

type interface Type {
    ...
    NumField() int
    Field(i int) StructField

    NumMethod() int
    Method(int) Method
    ...
}

也就是說,只要你能拿到 Type 型別的介面值,就可以知道結構體包含了幾個欄位,幾個方法。通過 NumField 和 Method 方法,你就可以獲取關於第 i 個 field 和 method 的具體資訊。

!!!注意:只有 Kind 為 Struct 的 Type 才可以呼叫上面這 4 個方法,否則程式會 panic.

field 和 method 的資訊是通過 StructField 和 Method 型別進行描述的:

type StructField struct {
    // Name is the field name.
    Name string
    // PkgPath is the package path that qualifies a lower case (unexported)
    // field name. It is empty for upper case (exported) field names.
// See https://golang.org/ref/spec#Uniqueness_of_identifiers PkgPath string Type Type // field type Tag StructTag // field tag string Offset uintptr // offset within struct, in bytes Index []int // index sequence for Type.FieldByIndex Anonymous bool // is an embedded field
} type Method struct { // Name is the method name. // PkgPath is the package path that qualifies a lower case (unexported) // method name. It is empty for upper case (exported) method names. // The combination of PkgPath and Name uniquely identifies a method // in a method set. // See https://golang.org/ref/spec#Uniqueness_of_identifiers Name string PkgPath string Type Type // method type Func Value // func with receiver as first argument Index int // index for Type.Method }

2. 示例

package main

import (
    "fmt"
    "reflect"
)

type Data struct {
    weight uint32
    height uint32
}

// 定義一個結構體
type Person struct {
    Name string `tips:this is name`
    age  int32  `how old are you?`
    Data
}

// 為 *Person 定義方法
func (p *Person) GetName() string {
    return p.Name
}

func (p *Person) GetAge() int32 {
    return p.age
}

func main() {
    p := Person{
        "allen",
        19,
        Data{
            50,
            180,
        },
    }

    // 1. 取到 type 介面值
    t := reflect.TypeOf(p)
    fmt.Println("欄位列舉:")
    fmt.Println("------------------------------")
    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)
        fmt.Printf("index:%d\nName:%s\nPkgPath:%s\nType:%v\nTag:%s\nOffset:%v\nIndex:%v\nAnonymous:%v\n",
            i, f.Name, f.PkgPath, f.Type, f.Tag, f.Offset, f.Index, f.Anonymous)
        fmt.Println("------------------------------")
    }

    fmt.Println("方法列舉:")
    fmt.Println("------------------------------")
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("index:%d\nName:%s\nPkgPath:%s\nType:%v\nFunc:%v\nIndex:%v\n",
            i, m.Name, m.PkgPath, m.Type, m.Func, m.Index)
        fmt.Println("------------------------------")
    }
}

輸出結果:

欄位列舉:
------------------------------
index:0
Name:Name
PkgPath:
Type:string
Tag:tips:this is name
Offset:0
Index:[0]
Anonymous:false
------------------------------
index:1
Name:age
PkgPath:main
Type:int32
Tag:how old are you?
Offset:16
Index:[1]
Anonymous:false
------------------------------
index:2
Name:Data
PkgPath:
Type:main.Data
Tag:
Offset:20
Index:[2]
Anonymous:true
------------------------------
方法列舉:
------------------------------

有同學會很好奇,為什麼方法一個都沒有遍歷出來?不知道你是否還記得“方法的接收器”相關的知識。在上面的例子裡,方法的接收器是指標型別,這意味著那兩個方法並沒有定義在 Person 型別上,而是定義在 *Person 上。

你使用 TypeOf(p) 拿到的型別是關於 Person 的型別資訊,而不是 *Person 型別資訊。因此,如果你想列舉方法,就只能使用 TypeOf(&p) 拿到關於 *Person 的型別資訊,才可以正確的枚舉出方法。

3. 如何列印欄位對應的值

欄位名也有了,那麼欄位的值怎麼獲取呢?

要知道,Type 介面只能拿到關於型別的資訊,無法拿到 Value 的資訊。如果想獲取欄位值,就只能從 Value 這個結構體入手了。同樣的,Value 也提供了 4 個方法:

type struct Value {
    ...
}

func (v Value) NumField() int
func (v Value) Field(i int) Value
func (v Value) NumMethod() int
func (v Value) Method(i int) Value

不過,這 4 個方法和 Type 介面那 4 個方法返回值不一樣,它返回的又是 Value 型別。好了,剩下的就是碼程式碼了,這個實在是太簡單,同學們自己完全可以寫出來。

4. 總結

  • 知道如何遍歷結構體的欄位和方法
  • 知道 Type 介面關於遍歷欄位和方法的方法與 Value 的區別