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
, 有三種情況例外:
-
零值表示沒有值 (聲明瞭但是未初始化)
-
如果 IsValid 方法返回 false, Kind 方法也返回 false
-
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{}
類型之外,也可以轉換爲具體的類型,這裏列舉一下 bool
和 bytes
, 其他類型以此類推:
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
只有滿足以下兩個條件時纔可以被修改:
-
可尋址
-
可導出的結構體字段
如果 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{} 得到反射對象
-
通過調用
reflect.ValueOf
方法,獲取到具體的值對應的reflect.Value
對象 -
通過調用
reflect.TypeOf
方法,獲取到具體的值對應的reflect.Type
對象
反射可以通過反射對象得到 interface{}
- 通過調用
reflect.Interface
方法,獲取到具體的值對應的interface{}
例如:
-
通過調用
reflect.Bool
方法,獲取到具體的值對應的bool
-
通過調用
reflect.Bytes
方法,獲取到具體的值對應的[]byte
修改反射對象的前提是值必須可以被修改
-
通過調用
reflect.Set
方法,修改反射對象 -
通過調用
mustbeassignable
,檢測反射對象是否可以修改 -
通過調用
mustbeexported
,確認反射對象是否可導出 -
通過調用
assignTo 方法
,修改反射對象並返回一個新對象 -
以上過程中如果任意條件不滿足,直接產生
panic
小結
基礎數據類型對象和 反射對象
之間的完全轉化需要兩個步驟:
-
基礎數據類型轉換爲
interface{}
-
interface{}
轉換爲反射對象
反過來,步驟正好逆向:
-
反射對象
轉換爲interface{}
-
interface{}
轉換爲基礎數據類型
反射
的內部實現分析完了,最後來總結下 反射
的優缺點及應用場景。
優點
-
避免硬編碼,提高靈活性
-
獲取代碼運行時能力,可以補足標準庫缺失的能力,如動態修改值、判斷是否實現接口、動態調用方法 (動態語言的特性) 等
缺點
-
有一定的學習和使用成本
-
降低代碼可讀性
-
降低代碼性能
-
規避了編譯器檢查,可能造成潛在的運行時
panic
或Bug
使用建議
-
框架內部可以使用,比如標準庫中的
encoding/json
包使用反射來解析數據類型,開源的ORM
框架中使用反射來獲取對象與數據表映射關係 -
普通代碼中不建議使用,尤其是位於
hot path
上面的業務代碼
Reference
-
反射 - 維基百科 [1]
-
Go 如何實現 implements[2]
-
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