Go 反射應用與三大定律

概述

在計算機學中,反射 (reflection) 是指計算機程序在運行時 (runtime) 可以訪問、檢測和修改它本身的狀態或行爲的一種能力。簡單來說,反射就是程序在運行的時候能夠觀察並且修改自己的行爲。

常見場景

Go 中 反射 常見的使用場景有 元數據編程確認接口實現機制

元數據編程

通過 反射 獲取字段標籤,實現簡單的元數據編程,比如 ORM 框架中的 Model 屬性。

package main

import (
 "fmt"
 "reflect"
)

type User struct {
 Name string `column:"username" type:"varchar(32)"`
 Age  int    `column:"age" type:"tinyint"`
}

func main() {
 var u User
 t := reflect.TypeOf(u)
 // 獲取 Name 字段對應的數據表字段
 if name, ok := t.FieldByName("Name"); ok {
  fmt.Printf("column = %s, type = %s\n",
   name.Tag.Get("column"),
   name.Tag.Get("type"),
  )
 }
 // 獲取 Age 字段對應的數據表字段
 if age, ok := t.FieldByName("Age"); ok {
  fmt.Printf("column = %s, type = %s\n",
   age.Tag.Get("column"),
   age.Tag.Get("type"),
  )
 }
}
// $ go run main.go
// 輸出如下
/**
    column = username, type = varchar(32)
    column = age, type = tinyint
*/

是否實現接口

Go 中沒有關鍵字 instanceof 來確認一個類型是否實現了某個接口,不過可以通過 reflect 包提供的 API 來確認。

package main

import (
 "fmt"
 "reflect"
)

type CustomStr interface {
 String() string
}

type CustomError struct{}

func (*CustomError) Error() string {
 return ""
}

func (*CustomError) String() string {
 return ""
}

type CustomStr2 interface {
 CustomStr
}

type CustomStr3 interface {
 String() string
}

func main() {
 // 獲取 error 類型
 typeOfError := reflect.TypeOf((*error)(nil)).Elem()
 // 獲取 CustomError 結構體類型
 customError := reflect.TypeOf(CustomError{})
 // 獲取 CustomError 結構體指針類型
 customErrorPtr := reflect.TypeOf(&CustomError{})

 // 判斷 CustomError 結構體類型是否實現了 error 接口
 fmt.Println(customError.Implements(typeOfError)) // false

 // 判斷 CustomError 結構體指針類型是否實現了 error 接口
 fmt.Println(customErrorPtr.Implements(typeOfError)) // true

 // 判斷 CustomError 結構體指針類型 (通過 nil 轉換來的) 是否實現了 error 接口
 fmt.Println(customErrorPtr.Implements(reflect.TypeOf((*CustomStr)(nil)).Elem())) // true

 // 判斷 CustomError 結構體指針類型是否實現了 error 接口 (嵌套的)
 fmt.Println(customErrorPtr.Implements(reflect.TypeOf((*CustomStr2)(nil)).Elem())) // true

 // 判斷 CustomError 結構體指針類型是否實現了 CustomStr3 接口
 fmt.Println(customErrorPtr.Implements(reflect.TypeOf((*CustomStr3)(nil)).Elem())) // true
}

// $ go run main.go
// 輸出如下
/**
    false
    true
    true
    true
    true
*/

內部實現

我們來探究一下 reflect 的內部實現文件目錄爲 $GOROOT/src/reflect,筆者的 Go 版本爲 go1.19 linux/amd64

反射類型

反射類型相關的結構體和方法在 type.go 文件內定義。

Type 接口

Type 接口用於數據類型和對應的反射方法的抽象表示。

Type 類型是可以比較的,例如使用 == 操作符,因此可以作爲 map 數據類型的 key, 如果兩個 Type 值表示相同的類型,則它們相等。

每個數據類型對應的方法都是不一樣的,反過來也一樣,每個方法不一定適用於所有數據類型,具體的限制在標準庫的文檔中都有註釋說明。

type Type interface {
 // 適用於所有數據類型的方法

 // 返回在內存中分配該類型時的對齊方式(以字節爲單位)
 Align() int

 // 當作爲結構體中的字段時,FieldAlign 返回類型時的對齊方式(以字節爲單位)
 FieldAlign() int

 // 返回類型的方法集合中第 i 個方法 (如果 i 越界,會導致 panic)
 // 對於非接口類型 T 或者 *T,返回的方法的 Type 和 Func 字段描述了一個函數,其第一個參數是接收方,只有導出的方法是可訪問的
 // 對於接口類型,返回的方法的 Type 字段給出了方法簽名,沒有接收方,Func 字段爲 nil
 // 返回的方法列表按照字典順序排序
 Method(int) Method

 // 返回在類型的方法集合中名字對應的方法,根據是否存在對應方法,返回 true/false
 // 對於非接口類型 T 或者 *T,返回的方法的 Type 和 Func 字段描述了一個函數,其第一個參數是接收方
 // 對於接口類型,返回的方法的 Type 字段給出了方法簽名,沒有接收方,Func 字段爲 nil
 MethodByName(string) (Method, bool)

 // 返回使用 Method 可訪問的方法的數量
 // 注意,NumMethod 只對接口類型計算未導出的方法
 NumMethod() int

 // 返回已定義類型在其包內的類型名稱
 // 對於沒有定義的類型返回空字符串
 Name() string

 // 返回已定義類型的包路徑,即唯一標識包的導入路徑,如 "encoding/base64"
 // 如果類型是預聲明 (也就是內置類型) 的 (string, error) 或者未定義的 (*T, struct{}[]int, 或者非定義類型的別名, 例如: type A int)
 // 則返回空字符串
 PkgPath() string

 // 返回存儲給定類型值所需要字節數,類似於調用 unsafe.Sizeof()
 Size() uintptr

 // 返回類型的字符串表示
 // 字符串表示可以使用縮短的包名 (例如: 用 base64 代替 "encoding/base64"),並且不保證在類型之間是唯一的
 // 如果需要測試類型標識,請直接比較類型
 String() string

 // Kind 返回類型的特定表示,詳情見 Kind 類型
 Kind() Kind

 // 返回類型是否實現了接口 u
 Implements(u Type) bool

 // 返回類型是否可以賦值給 u
 AssignableTo(u Type) bool

 // 返回類型是否可以轉換爲類型 u
 // 即使 ConvertibleTo 返回 true, 轉換過程依然可能發生 panic
 // 例如: 一個 slice []T 可以轉換爲 *[N]T, 但是如果它的長度小於 N, 發生 panic
 ConvertibleTo(u Type) bool

 // 返回類型是否可以比較
 // 即使 Comparable 返回 true, 比較過程依然可能發生 panic
 // 例如: interface values 可以比較,但是如果 interface 對應的動態類型不支持比較,panic
 Comparable() bool

 // 只適用於某些類型的方法,具體取決於 Kind
 // 具體的對應關係如下:
 //
 // Int*, Uint*, Float*, Complex*:  Bits
 // Array:                          Elem, Len
 // Chan:                           ChanDir, Elem
 // Func:                           In, NumIn, Out, NumOut, IsVariadic.
 // Map:                            Key, Elem
 // Pointer:                        Elem
 // Slice:                          Elem
 // Struct:                         Field, FieldByIndex, FieldByName, FieldByNameFunc, NumField

 // 返回類型的大小(以位爲單位)
 // 如果類型不符合上述規則,panic
 Bits() int

 // 返回 channel 類型的方向
 // 如果類型不是 channel, panic
 ChanDir() ChanDir

 // 返回函數類型的最後一個參數是否爲可變參數,如果是,t.In(t.NumIn() - 1) 返回參數的隱式實際類型 []T
 // 具體的例子:
 //
 // For concreteness, if t represents func(x int, y ... float64)then
 //
 // t.NumIn() == 2
 // t.In(0) is the reflect.Type for "int"
 // t.In(1) is the reflect.Type for "[]float64"
 // t.IsVariadic() == true
 //
 // 如果類型不是一個 Func, panic
 IsVariadic() bool

 // 返回類型的元素類型
 // 如果類型不符合上述規則,panic
 Elem() Type

 // 返回結構體類型的第 i 個字段
 // 如果類型不是結構體或參數 i 越界, panic
 Field(i int) StructField

 // 返回與索引對應的嵌套字段, 這相當於對每個索引 i 依次調用 Field
 // 如果類型不是結構體, panic
 FieldByIndex(index []int) StructField

 // 返回結構體指定名稱的字段,以及一個標記字段是否存在的 bool 值
 FieldByName(name string) (StructField, bool)

 // 返回滿足匹配函數的名稱的結構體字段,以及一個標記字段是否存在的 bool 值
 // FieldByNameFunc 會考慮結構體本身的字段,以及嵌入到結構體中的字段
 // 按照 BFS 算法,在包含滿足匹配函數的一個或多個字段的最外層嵌套處停止 (BFS: 由淺入深)
 // 如果該深度有多個字段滿足匹配函數,它們將相互抵消,FieldByNameFunc 不返回任何匹配
 // 這種行爲反映了 Go 在包含嵌入字段的結構體中處理名稱查找的方法
 FieldByNameFunc(match func(string) bool) (StructField, bool)

 // 返回函數類型的第 i 個參數的類型
 // 如果類型不是函數或者 i 越界, 拋出 panic
 In(i int) Type

 // 返回 map 類型的 key 類型
 // 如果類型不是 map, 拋出 panic
 Key() Type

 // 返回數組類型的長度
 // 如果類型不是數組, 拋出 panic
 Len() int

 // 返回結構體類型的字段數量
 // 如果類型不是結構體, 拋出 panic
 NumField() int

 // 返回函數類型的參數數量
 // 如果類型不是函數, 拋出 panic
 NumIn() int

 // 返回函數類型的返回值數量
 // 如果類型不是函數, 拋出 panic
 NumOut() int

 // Out 返回函數類型的第 i 個返回值
 // 如果類型不是函數, 拋出 panic
 // 如果 i 越界, 拋出 panic
 Out(i int) Type

 common() *rtype
 uncommon() *uncommonType
}

Kind 類型

Kind 類型表示一種特定類型,零值時表示無效類型,該類型將 Go 語言中所有數據類型通過常量生成器的方式定義出來。

type Kind uint

const (
 Invalid Kind = iota
 Bool
 Int

 ...

 String
 Struct
 UnsafePointer
)

rtype 對象

rtype 對象表示反射類型,是一個 Type 接口的通用實現,被嵌入在其他數據類型對象內。

type rtype struct {
 size       uintptr  // 類型佔用內存大小
 ptrdata    uintptr  // 類型可以包含指針的字節數
 hash       uint32   // 類型 Hash, 避免在 Hash 表中計算 (緩存的作用)
 tflag      tflag    // 類型標誌位
 align      uint8    // 內存對齊信息
 fieldAlign uint8    // 類型結構體字段對齊字節數
 kind       uint8    // 類型的 Kind 表示
 equal     func(unsafe.Pointer, unsafe.Pointer) bool // 比較兩個對象是否相等
 gcdata    *byte     // GC 類型數據
 str       nameOff   // 類型名稱偏移量
 ptrToThis typeOff   // 類型指針偏移量
}

rtype 對象的基礎上,基礎數據結構通過內嵌,再依次單獨定義,這裏列舉一下 數組切片, 其他類型以此類推:

// 數組類型
type arrayType struct {
 rtype
 elem  *rtype
 slice *rtype
 len   uintptr
}

// 切片類型
type sliceType struct {
 rtype
 elem *rtype
}

Typeof 函數

Typeof 函數返回參數 i 對應的動態類型的反射類型,如果參數 i 是一個 nil, 返回 nil

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

toType 函數

toType 函數將 *rtype 轉換爲一個反射類型,

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

反射值

反射值相關的結構體和方法在 value.go 文件內定義。

Value 對象

Value 對象表示值對應的 反射對象, 提供了獲取 / 設置值的方法。

在使用特定於值的方法之前,請先使用 Kind 方法查找值的適配性,調用不適配該類型的方法會導致運行時 panic, 有三種情況例外:

  1. 零值表示沒有值 (聲明瞭但是未初始化)

  2. 如果 IsValid 方法返回 false, Kind 方法也返回 false

  3. String 方法返回 ""

Value 所表示的值可以被多個 goroutine 併發使用,當然前提是該值類型本身支持直接併發操作 (例如 map 就不支持併發寫)。

type Value struct {
 // typ 表示由值表示的值類型
 typ *rtype

 // 指向指的指針,如果設置了 flagIndir, 則是指向數據的指針
 // 如果設置了 flagIndir 或 typ.pointers 方法返回 true 時有效
 ptr unsafe.Pointer

 // flag 持有 value 的元數據
 // 最低位表示如下:
 // - flagStickyRO: 通過未導出的非嵌入字段獲得, 因此是隻讀的
 // - flagEmbedRO:  通過未導出的嵌入字段獲得, 因此是隻讀的
 // - flagIndir:    持有指向數據的指針
 // - flagAddr:     v.CanAddr is true (表示設置了 flagIndir)
 // - flagMethod:   v 是方法值
 // 接下來的 5 個 bits 給出值的 Kind 類型
 // 除了方法值之外,它會重複 typ.Kind()

 // 其餘 23 位以上給出方法 values 的方法編號
 // 如果 flag.kind()!= Func,代碼可以假設沒有設置 flagMethod
 // 如果 ifaceIndir(typ),代碼可以假設 flagIndir 已設置
 flag
}

ValueOf 函數

ValueOf 函數返回一個存儲在參數接口 i 中,初始化後的具體的新值。

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

 // 所有數據總是逃逸到堆上
 // 控制生存週期更容易
 escapes(i)

 return unpackEface(i)
}

escapes 函數

escapes 函數保證參數變量逃逸到堆上。

// 學習小技巧: 控制變量逃逸到堆上
// Dummy 註解標記值 x 逃逸
func escapes(x any) {
 if dummy.b {
  dummy.x = x
 }
}

var dummy struct {
 b bool
 x any
}

emptyInterface 對象

emptyInterface 對象表示一個 interface{} 值的頭部。

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

ifaceIndir 函數

ifaceIndir 函數返回參數 t 是否間接存儲在一個 interface 值中。

func ifaceIndir(t *rtype) bool {
 return t.kind&kindDirectIface == 0
}

unpackEface 函數

unpackEface 函數將一個 emptyInterface 對象轉換爲一個 Value 對象。

func unpackEface(i any) Value {
 e := (*emptyInterface)(unsafe.Pointer(&i))

 // 注意: 在確認 e.word 是否爲一個指針之前,不要讀取
 // 避免賦值時 panic, Value 結構體第二個字段爲 unsafe.Pointer
 t := e.typ
 if t == nil {
  return Value{}
 }
 f := flag(t.Kind())
 if ifaceIndir(t) {
  f |= flagIndir
 }
 return Value{t, e.word, f}
}

CanInterface 方法

CanInterface 方法返回調用 Interface 方法是否不會產生 panic, 雖然返回值是 bool,但是函數內部卻可能直接 panic, 所以把兼容性處理的工作交給了調用方。

func (v Value) CanInterface() bool {
 if v.flag == 0 {
  panic(&ValueError{"reflect.Value.CanInterface", Invalid})
 }
 return v.flag&flagRO == 0
}

Interface 方法

Value 對象轉換爲一個 interface{} 類型返回 (Go 1.18 及之後是 any 類型,這裏爲了兼容,統一用 interface{} 來描述),如果 Value 對象是通過訪問未導出的結構體字段獲得的,拋出 panic

func (v Value) Interface() (i any) {
 return valueInterface(v, true)
}

Value 轉換爲具體類型

Value 除了可以將轉換爲 interface{} 類型之外,也可以轉換爲具體的類型,這裏列舉一下 boolbytes, 其他類型以此類推:

func (v Value) Bool() bool {
 if v.kind() != Bool {
  v.panicNotBool()
 }
 return *(*bool)(v.ptr)
}

func (v Value) Bytes() []byte {
 if v.typ == bytesType {
  return *(*[]byte)(v.ptr)
 }
 return v.bytesSlow()
}

Elem 方法

Elem 方法返回參數 v 作爲接口時包含的值,或參數 v 作爲指針時指向的值,如果 v 的 Kind 類型不是接口或指針,產生 panic, 如果 v == nil,返回 nil

func (v Value) Elem() Value {
 k := v.kind()
 switch k {
 case Interface:
    ...
 case Pointer:
  ...
 }

 panic(&ValueError{"reflect.Value.Elem", v.kind()})
}

CanSet 方法

CanSet 方法返回參數 v 是否可以被修改,一個 Value 只有滿足以下兩個條件時纔可以被修改:

  1. 可尋址

  2. 可導出的結構體字段

如果 CanSet 方法返回 false, 調用 Set 方法或其他特定類型的 Setter 方法時 (例如 SetBool, SetInt 等),產生 panic

func (v Value) CanSet() bool {
 return v.flag&(flagAddr|flagRO) == flagAddr
}

Set 方法

Set 方法將 Value 設置爲參數 x, 和數據類型轉換的基礎通用規則一樣,x 的值必須可以賦值給 v 的類型。

func (v Value) Set(x Value) {
 v.mustBeAssignable()    // 是否可以被設置
 x.mustBeExported()      // 是否對外公開
 ...
 x = x.assignTo("reflect.Set", v.typ, target)
 ...
}

mustBeAssignable 方法

mustBeAssignable 方法用於檢測賦值操作,如果不可賦值時產生 panic

func (f flag) mustBeAssignable() {
 ...
}

mustBeExported 方法

mustBeExported 方法用於檢測是否可導出,如果不可導出時產生 panic

func (f flag) mustBeExported() {
  ...
}

assignTo 方法

assignTo 方法返回一個直接分配到 dst 的 Value, 如果參數 v 是不可分配的,產生 panic

func (v Value) assignTo(context string, dst *rtype, target unsafe.Pointer) Value {
 ...
}

implements 方法

implements 方法返回類型 V 是否實現來接口 T。

func implements(T, V *rtype) bool {
 if T.Kind() != Interface {
    // 如果 T 不是接口類型
  return false
 }

 t := (*interfaceType)(unsafe.Pointer(T))
 if len(t.methods) == 0 {
    // 空的接口,任意類型都自動實現該接口
  return true
 }

 // 相同的算法適用於這兩種情況,但是接口類型和具體類型的方法表不同,因此代碼 (算法部分) 是重複的
 // 在兩種情況下,算法都是對兩個列表: T 的方法和 V 的方法,同時進行線性掃描 (時間複雜度 O(N))
 // 因爲方法表是按照唯一的順序存儲的 (字典順序,沒有重複的方法名)
 // 所以對 V 的方法的掃描必須與 T 的每個方法都匹配,否則 V 就沒有實現 T
 // 這樣可以在線性時間內進行掃描,而不是單純搜索所需的平方級複雜度
 if V.Kind() == Interface {
    // 接口類型判斷
  v := (*interfaceType)(unsafe.Pointer(V))
  i := 0

  for j := 0; j < len(v.methods); j++ {
      ...
   if vmName.name() == tmName.name() && V.typeOff(vm.typ) == t.typeOff(tm.typ) {
        ...
    if i++; i >= len(t.methods) {
     return true
    }
   }
  }
  return false
 }

  // 普通類型判斷
 v := V.uncommon()
 if v == nil {
  return false
 }
 i := 0
 vmethods := v.methods()
 for j := 0; j < int(v.mcount); j++ {
  ...
  if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) {
   ...
   if i++; i >= len(t.methods) {
    return true
   }
  }
 }
 return false
}

三大定律

反射可以通過 interface{} 得到反射對象

反射可以通過反射對象得到 interface{}

例如:

  1. 通過調用 reflect.Bool 方法,獲取到具體的值對應的 bool

  2. 通過調用 reflect.Bytes 方法,獲取到具體的值對應的 []byte

修改反射對象的前提是值必須可以被修改

小結

基礎數據類型對象和 反射對象 之間的完全轉化需要兩個步驟:

  1. 基礎數據類型轉換爲 interface{}

  2. interface{} 轉換爲 反射對象

反過來,步驟正好逆向:

  1. 反射對象 轉換爲 interface{}

  2. interface{} 轉換爲基礎數據類型

反射 的內部實現分析完了,最後來總結下 反射 的優缺點及應用場景。

優點

缺點

使用建議

Reference

  1. 反射 - 維基百科 [1]

  2. Go 如何實現 implements[2]

  3. Go 的面向對象編程 [3]

參考資料

[1]

反射 - 維基百科: https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%B0%84%E5%BC%8F%E7%BC%96%E7%A8%8B

[2]

Go 如何實現 implements: https://dbwu.tech/posts/golang_implements/

[3]

Go 的面向對象編程: https://dbwu.tech/posts/golang_oop/

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