大量實例詳解 Go 反射機制原理與應用

有一些高級語言提供了涉及到編程元素深層信息的接口,這些信息通常是運行時或編譯器有用,但語言也通過接口將其暴露出來,這樣開發者就能使用它們實現一些類似黑客的功能。這些能讓開發者攫取到編程元素深層信息或者進行深度操作的接口就叫反射,在 Go 和 Java 都有提供,運用好反射功能可以開發出功能強大的程序,但是反射由於涉及到編譯原理,因此比較抽象,在此我們用豐富的例子來說清楚 GO 的反射接口應用。

Go 的反射接口來自於 reflect 包,其中大部分反射功能都來自 3 個對象,分別爲 reflect.Type, reflect.Value, reflect.Kind。其中 Type 用來獲取變量對象的類型信息,Value 用來獲取變量的值,Kind 跟 Type 類似,也是獲得變量類型信息,但是跟 Type 有所不同,我們看個具體例子:

import (
    "fmt"
    "reflect"
)

func main() {
    var s string
    s = "hello"
    s_type := reflect.TypeOf(s) //返回 reflect.Type
    fmt.Println("s_type:", s_type)   //輸出 string
    s_value := reflect.ValueOf(s) //返回reflect.Value
    fmt.Println("s_value:", s_value)  //輸出 hello
    s_kind := s_value.Kind() //返回 reflect.Kind 
    fmt.Println("s_kind:", s_kind) //輸出 string
}

reflect.TypeOf 返回的類型爲 reflect.Type 對象,reflect.ValueOf 返回 reflect.Value 對象,reflect.Value.Kind 返回 reflect.Kind 對象。從上面代碼的返回看不好分辨 reflect.Type 和 reflect.Kind 的區別,我們再看一個例子:

import (
    "fmt"
    "reflect"
)

type Employee struct {
    Name    string
    Age     uint32
    Salary  float32
    Has_pet bool
}

func main() {
    var e Employee
    e_value := reflect.ValueOf(e)
    fmt.Println("e_value: ", e_value)
    e_type := e_value.Type()
    fmt.Println("e_type: ", e_type)
    e_kind := e_value.Kind()
    fmt.Println("e_kind: ", e_kind)
}

上面代碼運行後輸出結果爲:

e_value:  { 0 0 false}
e_type:  main.Employee
e_kind:  struct

從結果可以看到, reflect.Value 對應變量在內存中的數據,在 GO 中,任何變量定義後會根據其類型初始化爲 0,所以結構體的每個字段初始化成了相應類型的 nil,由於 string 對應的”nil” 是空字符串,因此打印時沒有輸出任何內容。同時我們看到通過 reflect.Value 對象的 Type 接口能獲取對應的 reflect.Type 對象,通過 Kind 接口能獲取 reflect.Kind 對象。

我們再看 Type 與 Kind 的區別,Type 對應關鍵字 type 後面的定義,從例子中看就是 main.Employee, Kind 對應變量的在 GO 語言中的基礎類型,在 GO 也就有那麼幾種基礎類型,分別爲 int, uint, float, slice, map, string, bool, ptr , 本質上 reflect.Kind 可以看做是一個枚舉類型的對象,通過它我們可以辨別對象的基礎類型然後採取相應的操作。

reflect.Type 的作用很大,它最重要的作用在於解析複合類型,例如解析 struct 類型裏面的各個字段,解讀 slice 類型每個元素的 Type 信息等,我們看看如何通過 reflect.Type 讀取 struct 內部各個字段的信息,例子如下:

package main

import (
    "fmt"
    "reflect"
)

type MyData struct {
    Name   string `csv:"name"`
    HasPet bool   `csv:"has_pet"`
    Age    int    `csv:"age"`
    Salary float32
}

func main() {
    myData := MyData{
        Name:   "Smith",
        Age:    45,
        HasPet: false,
        Salary: 5000.0,
    }

    myDataValue := reflect.ValueOf(myData)
    myDataType := myDataValue.Type()

    for i := 0; i < myDataType.NumField(); i++ { //NumField獲得結構體裏面字段的數量
        structField := myDataType.Field(i)                    //Field用於獲得字段的reflect.StructField對象
        fmt.Println("field name : ", structField.Name)        //reflect.StructField.Name獲得字段名
        fmt.Println("field type : ", structField.Type.Name()) //reflect.StructField.Type.Name ()獲得字段類型
        if tag, ok := structField.Tag.Lookup("csv"); ok {
            fmt.Println("field tag: ", tag)
        }

        fieldVal := myDataValue.Field(i) //reflect.Value同樣支持Field接口
        switch fieldVal.Kind() {         //判斷其基礎類型
        case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
            fmt.Println("field with value: ", fieldVal.Int())
        case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
            fmt.Println("field with value: ", fieldVal.Uint())
        case reflect.String:
            fmt.Println("field  with value: ", fieldVal.String())
        case reflect.Bool:
            fmt.Println("field with value: ", fieldVal.Bool())
        case reflect.Float32, reflect.Float64:
            fmt.Println("field with value: ", fieldVal.Float())
        }
    }

}

上面代碼運行後輸出結果爲:

field name :  Name
field type :  string
field tag:  name
field  with value:  Smith
field name :  HasPet
field type :  bool
field tag:  has_pet
field with value:  false
field name :  Age
field type :  int
field tag:  age
field with value:  45
field name :  Salary
field type :  float32
field with value:  5000

從代碼可以看到,如果對象類型爲結構體,那麼可以調用 reflect.Type 對象的 NumField 接口獲得結構體裏面的字段數量,然後通過它的 Field 接口返回 reflect.StructField 類型,reflect.StructField.Name 對應字段的名稱,reflect.StructField.Type.Name() 返回字段的數據定義類型,同時 reflect.StructField.Tag.LookUp 可以查找字段是否含有給定標籤,如果有的話還能返回標籤對應的內容.

同時我們還能看到,reflect.Value 對象也支持 Field 接口,它得到各個字段對應的 reflect.Value 對象,然後通過 Kind 接口返回信息來判斷字段的基礎類型,根據基礎類型調用不同方法來獲得對應數據,這裏要注意如果基礎類型是 string, 但是你調用 reflect.Value.Int() 來獲取數據,那麼就有可能造成 panic。同時調用 reflect.Type.NumField 接口時,必須確保對象是結構體類型,要不然調用該接口也會導致 panic。

這裏我們可以進一步分析 Go 的 interface 類型,interface 其實包含了兩部分,一部分是 reflect.Type, 一部分是 reflect.Value,如果一個 interface 對象是 nil 的話,它必須滿足 reflect.Type 對應 nil, 同時 reflect.Value.IsValid 返回 false, 例如:

import (
    "fmt"
    "reflect"
)

func main() {
    var i interface{}
    if i == nil {
        fmt.Println("type for nil interface: ", reflect.TypeOf(i))
        fmt.Println("is value valid for nil interface: ", reflect.ValueOf(i).IsValid())
    }

    strPtr := (*string)(nil)
    i = (interface{})(strPtr) //此時i不再是nil,因爲它有了reflect.Type內容
    fmt.Println("is interface i is nil: ", i == nil)
    fmt.Println("type for interface i is: ", reflect.TypeOf(i))
}

上面代碼運行後返回結果爲:

type for nil interface:  <nil>
is value valid for nil interface:  false
is interface i is nil:  false
type for interface i is:  *string

注意到雖然我們把一個空的字符串指針賦值給 i,但此時 i 不再是空接口,因爲它的 reflect.Type 部分有了內容,現在我們可以明白,爲何 interface 類型能指向所有其他類型呢,原因正是我們這裏解讀的反射原理,通過它的 reflect.Type 部分獲得它指向對象的類型,通過 reflect.Value 部分來讀取對象的內容。

還有兩種 “過渡” 類型需要我們注意,那就是指針和數組或者說是切片,之所以說是 “過渡” 是因爲指針類型對應的 Kind 是 reflect.Ptr,它還需要做進一步操作才能獲得指針指向對象的具體類型。同時切片類型對應的 Kind 是 reflect.Slice,我們還需要進一步操作才能獲得切片元素的類型。如果對象是指針或者切片類型,那麼 reflect.Type.Elem()就能獲得指向元素類型或者是獲得切片所包含元素的類型,同時 reflect.Value.Elem()才能獲得指針指向對象的數值,對於切片類型,要獲得其包含元素的數值還需要一些額外操作,我們看下面例子:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    i := 1234
    i_ptr := &i
    i_ptr_type := reflect.TypeOf(i_ptr)
    i_ptr_value := reflect.ValueOf(i_ptr)
    i_ptr_kind := i_ptr_value.Kind()
    fmt.Println("i_ptr_type :", i_ptr_type)
    fmt.Println("i_ptr_value :", i_ptr_value)
    fmt.Println("i_ptr_kind: ", i_ptr_kind)
    fmt.Println("element type for element pointed to by i_ptr : ", i_ptr_type.Elem())
    fmt.Println("element value for pointer i_ptr: ", i_ptr_value.Elem())
}

上面代碼運行後輸出結果爲:

i_ptr_type : *int
i_ptr_value : 0xc000018030
i_ptr_kind:  ptr
element type for element pointed to by i_ptr :  int
element value for pointer i_ptr:  1234

可以看到指針類型是 *int,指針對應的值就是變量在內存中的地址,通過 Elem 調用獲得了指針指向元素的類型和數值,要注意如果元素的類型存在 “過渡” 屬性,那麼調用 Elem 就會導致 panic,接下來我們看看切片的解析:

import (
    "fmt"
    "reflect"
)

func main() {
    str_slice := []string{"hello", "world"}
    str_slice_value := reflect.ValueOf(str_slice)
    str_slice_type := reflect.TypeOf(str_slice)
    elem_type := str_slice_type.Elem() //獲得數組元素的類型信息
    fmt.Println("elem type :", elem_type)
    for i := 0; i < str_slice_value.Len(); i++ { //reflect.Value.Len() 獲得數組元素個數
        elem_value := str_slice_value.Index(i) //通過Index獲得指定元素
        fmt.Println("elem_value: ", elem_value)
    }
}

上面代碼運行後結果如下:

elem type : string
elem_value:  hello
elem_value:  world

代碼中需要注意的是,如果元素類型不是切片或數組,那麼調用 reflect.Value.Len 接口會導致 panic。最後我們看看 GO 內存分配,reflect.New 可以針對指針類型分配內存,我們看下面代碼示例:

import (
    "fmt"
    "reflect"
)

func main() {
    var iPtr *int
    fmt.Println("value of iPtr : ", reflect.ValueOf(iPtr))
    elem_type := reflect.TypeOf(iPtr).Elem() //獲得指針指向的元素類型
    elem_ptr := reflect.New(elem_type)       //這裏得到一個int類型指針對應的reflect.Value
    fmt.Println("elem ptr value: ", elem_ptr)
    elem_value := elem_ptr.Elem() //獲得指針指向的元素對於的reflect.Value對象
    elem_value.SetInt(20)         //調用reflect.Value 對應接口設置元素數值
    fmt.Println("elem value: ", elem_value)
}

上面代碼運行後結果爲:

value of iPtr :  <nil>
elem ptr value:  0xc000018040
elem value:  20

這裏需要注意的是 reflect.New 返回的是指針類型對象對應的 reflect.Value,由於它具有 “過渡” 屬性,因此我們可以調用 Elem 獲得指針指向對象的 reflect.Value 對象,然後調用相應的 Set 接口來設置對象的數值,如果對象類型是 int 那麼就實現 SetInt,如果是 string,那就使用 SetString, 但如果對象類型是 int, 但使用 SetString 就會導致 panic。

反射由於涉及到編譯原理等因素,如果沒有相應代碼示例來輔助理解,那麼我們學起來會感覺很抽象和燒腦,希望上面代碼示例能幫助同學們對 GO 的反射機制有較好的理解。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VrdEduATbLHHsB_yw0cpCw