【Go進階—基礎特性】反射

與昊發表於2022-03-21

定義

反射是什麼?來自維基百科的定義是:在計算機學中,反射是指計算機程式在執行時可以訪問、檢測和修改它本身狀態或行為的一種能力。用比喻來說,反射就是程式在執行的時候能夠 “觀察” 並且修改自己的行為。《Go 程式設計語言》中則是這樣定義的:Go 語言提供了一種機制在執行時更新變數和檢查它們的值、呼叫它們的方法,但是在編譯時並不知道這些變數的具體型別,這稱為反射機制。

使用場景和缺點

使用反射的常見場景有兩種:

  • 函式的引數型別不能確定,需要在執行時處理任意物件。
  • 不能確定呼叫哪個函式,需要根據傳入的引數在執行時決定。

反射機制能帶給我們很大的靈活性,不過絕大多數情況下還是不推薦使用,主要理由有:

  • 與反射相關的程式碼一般可讀性都比較差。
  • 反射相關的程式碼通常在執行時才會暴露錯誤,這時經常是直接 panic,可能會造成嚴重的後果。
  • 反射對效能影響比較大,比正常程式碼執行速度慢一到兩個數量級。

所以這是一把雙刃劍,因此要想使用好,必須得明白其中的基本原理。

實現原理

reflect.TypeOf 和 reflect.ValueOf 是進入反射世界僅有的兩扇“大門”。顧名思義,這兩個函式返回的分別是介面變數的型別資訊和值資訊,首先來看一下這兩個函式。

reflect.TypeOf

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

注:傳入反射函式如 TypeOf 等的引數(interface 型別)是在編譯階段進行型別轉換的。

emptyInterface 和上一篇文章中提到的空介面型別 eface 是一樣的,只是屬性名不同而已,typ 是介面變數的動態型別,word 是動態值的指標。那麼這個函式的邏輯就很明白了:使用 unsafe.Pointer 方法獲取介面變數的指標值,將之強制轉換為 emptyInterface 型別,並返回它的動態型別。

返回值 Type 實際上是一個介面,定義了很多方法,用來獲取型別相關的各種資訊,而 *rtype 實現了 Type 介面。Type 定義了很多有關型別的方法,建議都大致過一遍。

type Type interface {
    // 所有的型別都可以呼叫下面這些函式
    // 此型別的變數對齊後所佔用的位元組數
    Align() int
    // 如果是 struct 的欄位,對齊後佔用的位元組數
    FieldAlign() int
    // 返回型別方法集裡的第 `i` (傳入的引數)個方法
    Method(int) Method
    // 通過名稱獲取方法
    MethodByName(string) (Method, bool)
    // 獲取型別方法集裡匯出的方法個數
    NumMethod() int
    // 型別名稱
    Name() string
    // 返回型別所在的路徑,如:encoding/base64
    PkgPath() string
    // 返回型別的大小,和 unsafe.Sizeof 功能類似
    Size() uintptr
    // 返回型別的字串表示形式
    String() string
    // 返回型別的型別值
    Kind() Kind
    // 型別是否實現了介面 u
    Implements(u Type) bool
    // 是否可以賦值給 u
    AssignableTo(u Type) bool
    // 是否可以型別轉換成 u
    ConvertibleTo(u Type) bool
    // 型別是否可以比較
    Comparable() bool
    // 下面這些函式只有特定型別可以呼叫
    // 如:Key, Elem 兩個方法就只能是 Map 型別才能呼叫
    // 型別所佔據的位數
    Bits() int
    // 返回通道的方向,只能是 chan 型別呼叫
    ChanDir() ChanDir
    // 返回型別是否是可變引數,只能是 func 型別呼叫
    // 比如 t 是型別 func(x int, y ... float64)
    // 那麼 t.IsVariadic() == true
    IsVariadic() bool
    // 返回內部子元素型別,只能由型別 Array, Chan, Map, Ptr, or Slice 呼叫
    Elem() Type
    // 返回結構體型別的第 i 個欄位,只能是結構體型別呼叫
    // 如果 i 超過了總欄位數,就會 panic
    Field(i int) StructField
    // 返回巢狀的結構體的欄位
    FieldByIndex(index []int) StructField
    // 通過欄位名稱獲取欄位
    FieldByName(name string) (StructField, bool)
    // FieldByNameFunc returns the struct field with a name
    // 返回名稱符合 func 函式的欄位
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // 獲取函式型別的第 i 個引數的型別
    In(i int) Type
    // 返回 map 的 key 型別,只能由型別 map 呼叫
    Key() Type
    // 返回 Array 的長度,只能由型別 Array 呼叫
    Len() int
    // 返回型別欄位的數量,只能由型別 Struct 呼叫
    NumField() int
    // 返回函式型別的輸入引數個數
    NumIn() int
    // 返回函式型別的返回值個數
    NumOut() int
    // 返回函式型別的第 i 個值的型別
    Out(i int) Type
    // 返回型別結構體的相同部分
    common() *rtype
    // 返回型別結構體的不同部分
    uncommon() *uncommonType
}

reflect.ValueOf

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }

    escapes(i)

    return unpackEface(i)
}

func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

ValueOf 的邏輯也比較簡單:首先呼叫 escapes 讓變數 i 逃逸到堆上,然後將變數 i 強制轉換為 emptyInterface 型別,最後將所需的資訊組裝成 reflect.Value 型別後返回。reflect.Value 的結構和 emptyInterface 也很像,只是多個表示後設資料的 flag 欄位。

reflect.Value 結構體的方法同樣很多,建議都大致瀏覽一下。

reflect.Set

當我們想要更新 reflect.Value 時,就需要呼叫 reflect.Value.Set 更新反射物件:

func (v Value) Set(x Value) {
    v.mustBeAssignable()
    x.mustBeExported() 
    var target unsafe.Pointer
    if v.kind() == Interface {
        target = v.ptr
    }
    x = x.assignTo("reflect.Set", v.typ, target)
    if x.flag&flagIndir != 0 {
        if x.ptr == unsafe.Pointer(&zeroVal[0]) {
            typedmemclr(v.typ, v.ptr)
        } else {
            typedmemmove(v.typ, v.ptr, x.ptr)
        }
    } else {
        *(*unsafe.Pointer)(v.ptr) = x.ptr
    }
}

程式碼邏輯可以分為四個步驟:

  1. 檢查當前反射物件及其欄位是否可以被設定。
  2. 檢查待設定的 Value 物件是否可匯出。
  3. 呼叫 assignTo 方法建立一個新的 Value 物件並對原本的 Value 物件進行覆蓋。
  4. 根據返回的 Value 物件的指標值,對當前反射物件的指標值進行修改。

反射包中還有其他方法,此時就不一一列舉了,可以根據下面這張圖來串起 interface、Type 和 Value:

image.png

反射三大定律

Go 官方關於反射的部落格,介紹了反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

反射的第一定律是:“反射可以從介面值(interface)得到反射物件”。當我們執行 reflect.ValueOf(1) 時,雖然看起來是獲取了基本型別 int 對應的反射物件,但是由於 reflect.TypeOf、reflect.ValueOf 兩個方法的入參都是 interface{} 型別,所以在方法執行的過程中發生了型別轉換。

反射的第二定律是:“可以從反射物件得到介面值(interface)”。這是與第一條定律相反的定律,既然能夠將介面型別的變數轉換成反射物件,那麼一定需要其他方法將反射物件還原成介面型別的變數,reflect 中的 reflect.Value.Interface 就能完成這項工作。

反射的第三定律是:“要修改反射物件,該值必須可以修改”。這一條稍微有點複雜,我們來看個例子,新手在使用反射的時候可能會犯下面這種錯誤:

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

// 執行結果
// panic: reflect: reflect.Value.SetInt using unaddressable value

從錯誤資訊我們可以看出,我們在修改變數 i 的值的時候使用了非可定址的值,也就是說該值不可以修改。這麼做的原因在於,Go 語言中函式的引數都是採用值傳遞,也就是傳遞了值的一份副本,此時肯定無法根據這份副本來修改反射物件的值。因此 Go 標準庫就對其進行了邏輯判斷,避免出現問題。

所以我們只能用間接的方式改變原變數的值:先獲取指標對應的 reflect.Value,再通過 reflect.Value.Elem 方法得到可以被設定的變數,最後呼叫 Set 相關方法來進行設定。

相關文章