定義
反射是什麼?來自維基百科的定義是:在計算機學中,反射是指計算機程式在執行時可以訪問、檢測和修改它本身狀態或行為的一種能力。用比喻來說,反射就是程式在執行的時候能夠 “觀察” 並且修改自己的行為。《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
}
}
程式碼邏輯可以分為四個步驟:
- 檢查當前反射物件及其欄位是否可以被設定。
- 檢查待設定的 Value 物件是否可匯出。
- 呼叫 assignTo 方法建立一個新的 Value 物件並對原本的 Value 物件進行覆蓋。
- 根據返回的 Value 物件的指標值,對當前反射物件的指標值進行修改。
反射包中還有其他方法,此時就不一一列舉了,可以根據下面這張圖來串起 interface、Type 和 Value:
反射三大定律
Go 官方關於反射的部落格,介紹了反射有三大定律:
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- 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 相關方法來進行設定。