天天看點

GoLang反射

GoLang反射

reflect包實作了運作時反射,允許程式操作任意類型的對象。典型用法是用靜态類型interface{}儲存一個值,通過調用TypeOf擷取其動态類型資訊,該函數傳回一個Type類型值。調用ValueOf函數傳回一個Value類型值,該值代表運作時的資料。Zero接受一個Type類型參數并傳回一個代表該類型零值的Value類型值。
GoLang反射

反射是 Go 語言比較重要的特性。雖然在大多數的應用和服務中并不常見,但是很多架構都依賴 Go 語言的反射機制實作簡化代碼的邏輯。因為 Go 語言的文法元素很少、設計簡單,是以它沒有特别強的表達能力,但是 Go 語言的 reflect 包能夠彌補它在文法上的一些劣勢。

reflect 實作了運作時的反射能力,能夠讓程式操作不同類型的對象。反射包中有兩對非常重要的函數和類型,reflect.TypeOf 能擷取類型資訊,reflect.ValueOf 能擷取資料的運作時表示,另外兩個類型是 Type 和 Value,它們與函數是一一對應的關系:

類型 Type 是反射包定義的一個接口,我們可以使用 reflect.TypeOf 函數擷取任意變量的的類型,Type 接口中定義了一些有趣的方法,MethodByName 可以擷取目前類型對應方法的引用、Implements 可以判斷目前類型是否實作了某個接口:

type Type interface {
        Align() int
        FieldAlign() int
        Method(int) Method
        MethodByName(string) (Method, bool)
        NumMethod() int
        ...
        Implements(u Type) bool
        ...
}      

反射包中 Value 的類型與 Type 不同,它被聲明成了結構體。這個結構體沒有對外暴露的字段,但是提供了擷取或者寫入資料的方法:

type Value struct {
        // contains filtered or unexported fields
}
func (v Value) Addr() Value
func (v Value) Bool() bool
func (v Value) Bytes() []byte
...      

反射包中的所有方法基本都是圍繞着 Type 和 Value 這兩個類型設計的。我們通過 reflect.TypeOf、reflect.ValueOf 可以将一個普通的變量轉換成『反射』包中提供的 Type 和 Value,随後就可以使用反射包中的方法對它們進行複雜的操作。

三大法則

運作時反射是程式在運作期間檢查其自身結構的一種方式。反射帶來的靈活性是一把雙刃劍,反射作為一種元程式設計方式可以減少重複代碼,但是過量的使用反射會使我們的程式邏輯變得難以了解并且運作緩慢。我們在這一節中會介紹 Go 語言反射的三大法則,其中包括:

從 interface{} 變量可以反射出反射對象;

從反射對象可以擷取 interface{} 變量;

要修改反射對象,其值必須可設定;

第一法則

反射的第一法則是我們能将 Go 語言的 interface{} 變量轉換成反射對象。很多讀者可能會對這以法則産生困惑 ,為什麼是從 interface{} 變量到反射對象?

當我們執行 reflect.ValueOf(1) 時,雖然看起來是擷取了基本類型 int 對應的反射類型,但是由于 reflect.TypeOf、reflect.ValueOf 兩個方法的入參都是 interface{} 類型,是以在方法執行的過程中發生了類型轉換。

Go 語言的函數調用都是值傳遞的,變量會在函數調用時進行類型轉換。基本類型 int 會轉換成 interface{} 類型,這也就是為什麼第一條法則是『從接口到反射對象』。

上面提到的 reflect.TypeOf 和 reflect.ValueOf 函數就能完成這裡的轉換,如果我們認為 Go 語言的類型和反射類型處于兩個不同的『世界』,那麼這兩個函數就是連接配接這兩個世界的橋梁。

GoLang反射

我們通過以下例子簡單介紹這兩個函數的作用,reflect.TypeOf 擷取了變量 author 的類型,reflect.ValueOf 擷取了變量的值 draven。如果我們知道了一個變量的類型和值,那麼就意味着知道了這個變量的全部資訊。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    author := "draven"
    fmt.Println("TypeOf author:", reflect.TypeOf(author))
    fmt.Println("ValueOf author:", reflect.ValueOf(author))
}

$ go run main.go
TypeOf author: string
ValueOf author: draven      

有了變量的類型之後,我們可以通過 Method 方法獲得類型實作的方法,通過 Field 擷取類型包含的全部字段。對于不同的類型,我們也可以調用不同的方法擷取相關資訊:

結構體:擷取字段的數量并通過下标和字段名擷取字段 StructField;

哈希表:擷取哈希表的 Key 類型;

函數或方法:擷取入參和傳回值的類型;

總而言之,使用 reflect.TypeOf 和 reflect.ValueOf 能夠擷取 Go 語言中的變量對應的反射對象。一旦擷取了反射對象,我們就能得到跟目前類型相關資料和操作,并可以使用這些運作時擷取的結構執行方法。

第二法則

反射的第二法則是我們可以從反射對象可以擷取 interface{} 變量。既然能夠将接口類型的變量轉換成反射對象,那麼一定需要其他方法将反射對象還原成接口類型的變量,reflect 中的 reflect.Value.Interface 方法就能完成這項工作:

GoLang反射

不過調用 reflect.Value.Interface 方法隻能獲得 interface{} 類型的變量,如果想要将其還原成最原始的狀态還需要經過如下所示的顯式類型轉換:

v := reflect.ValueOf(1)
v.Interface().(int)      

從反射對象到接口值的過程就是從接口值到反射對象的鏡面過程,兩個過程都需要經曆兩次轉換:

從接口值到反射對象:

  • 從基本類型到接口類型的類型轉換;
  • 從接口類型到反射對象的轉換;

從反射對象到接口值:

  • 反射對象轉換成接口類型;
  • 通過顯式類型轉換變成原始類型;
GoLang反射

當然不是所有的變量都需要類型轉換這一過程。如果變量本身就是 interface{} 類型,那麼它不需要類型轉換,因為類型轉換這一過程一般都是隐式的,是以我不太需要關心它,隻有在我們需要将反射對象轉換回基本類型時才需要顯式的轉換操作。

第三法則

Go 語言反射的最後一條法則是與值是否可以被更改有關,如果我們想要更新一個 reflect.Value,那麼它持有的值一定是可以被更新的,假設我們有以下代碼:

func main() {
    i := 1
    v := reflect.ValueOf(i)
    v.SetInt(10)
    fmt.Println(i)
}

$ go run reflect.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x82, 0x1014c0)
    /usr/local/go/src/reflect/value.go:247 +0x180
reflect.flag.mustBeAssignable(...)
    /usr/local/go/src/reflect/value.go:234
reflect.Value.SetInt(0x100dc0, 0x414020, 0x82, 0x1840, 0xa, 0x0)
    /usr/local/go/src/reflect/value.go:1606 +0x40
main.main()
    /tmp/sandbox590309925/prog.go:11 +0xe0      

運作上述代碼會導緻程式崩潰并報出 reflect: reflect.flag.mustBeAssignable using unaddressable value 錯誤,仔細思考一下就能夠發現出錯的原因,Go 語言的函數調用都是傳值的,是以我們得到的反射對象跟最開始的變量沒有任何關系,是以直接對它修改會導緻崩潰。

想要修改原有的變量隻能通過如下的方法:

func main() {
    i := 1
    v := reflect.ValueOf(&i)
    v.Elem().SetInt(10)
    fmt.Println(i)
}
$ go run reflect.go
10      

調用 reflect.ValueOf 函數擷取變量指針;

調用 reflect.Value.Elem 方法擷取指針指向的變量;

調用 reflect.Value.SetInt 方法更新變量的值:由于 Go 語言的函數調用都是值傳遞的,是以我們隻能先擷取指針對應的 reflect.Value,再通過 reflect.Value.Elem 方法迂回的方式得到可以被設定的變量,我們通過如下所示的代碼了解這個過程:

func main() {
    i := 1
    v := &i
    *v = 10
}      

如果不能直接操作 i 變量修改其持有的值,我們就隻能擷取 i 變量所在位址并使用 *v 修改所在位址中存儲的整數。

參考文章

​​傳送門1​​

繼續閱讀