解密 Go 語言之反射 Reflect
本文轉(zhuǎn)載自微信公眾號「腦子進煎魚了」,作者陳煎魚 。轉(zhuǎn)載本文請聯(lián)系腦子進煎魚了公眾號。
大家好,我是煎魚。今天是 2020 年的最后一天,讓我們一起繼續(xù)愉快的學習吧 :)。
在所有的語言中,反射這一功能基本屬于必不可少的模塊。
雖說 “反射” 這個詞讓人根深蒂固,但更多的還是 WHY。反射到底是什么,反射又是基于什么法則實現(xiàn)的?
今天我們通過這篇文章來一一揭曉,以 Go 語言為例,了解反射到底為何物,其底層又是如何實現(xiàn)的。
反射是什么
在計算機學中,反射是指計算機程序在運行時(runtime)可以訪問、檢測和修改它本身狀態(tài)或行為的一種能力。
用比喻來說,反射就是程序在運行的時候能夠 “觀察” 并且修改自己的行為(來自維基百科)。
簡單來講就是,應用程序能夠在運行時觀察到變量的值,并且能夠修改他。
一個例子
最常見的 reflect 標準庫例子,如下:
- import (
- "fmt"
- "reflect"
- )
- func main() {
- rv := []interface{}{"hi", 42, func() {}}
- for _, v := range rv {
- switch v := reflect.ValueOf(v); v.Kind() {
- case reflect.String:
- fmt.Println(v.String())
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- fmt.Println(v.Int())
- default:
- fmt.Printf("unhandled kind %s", v.Kind())
- }
- }
- }
輸出結(jié)果:
- hi
- 42
- unhandled kind func
在程序中主要是聲明了 rv 變量,變量類型為 interface{},其包含 3 個不同類型的值,分別是字符串、數(shù)字、閉包。
而在使用 interface{} 時常見于不知道入?yún)⒄呔唧w的基本類型是什么,那么我們就會用 interface{} 類型來做一個偽 “泛型”。
此時又會引出一個新的問題,既然入?yún)⑹?interface{},那么出參時呢?
Go 語言是強類型語言,入?yún)⑹?interface{},出參也肯定是跑不了的,因此必然離不開類型的判斷,這時候就要用到反射,也就是 reflect 標準庫。反射過后又再進行 (type) 的類型斷言。
這就是我們在編寫程序時最常遇見的一個反射使用場景。
Go reflect
reflect 標準庫中,最核心的莫過于 reflect.Type 和 reflect.Value 類型。而在反射中所使用的方法都圍繞著這兩者進行,其方法主要含義如下:
- TypeOf 方法:用于提取入?yún)⒅档念愋托畔ⅰ?/li>
- ValueOf 方法:用于提取存儲的變量的值信息。
reflect.TypeOf
演示程序:
- func main() {
- blog := Blog{"煎魚"}
- typeof := reflect.TypeOf(blog)
- fmt.Println(typeof.String())
- }
輸出結(jié)果:
- main.Blog
從輸出結(jié)果中,可得出 reflect.TypeOf 成功解析出 blog 變量的類型是 main.Blog,也就是連 package 都知道了。
通過人識別的角度來看似乎很正常,但程序就不是這樣了。他是怎么知道 “他” 是哪個 package 下的什么呢?
我們一起追一下源碼看看:
- func TypeOf(i interface{}) Type {
- eface := *(*emptyInterface)(unsafe.Pointer(&i))
- return toType(eface.typ)
- }
從源碼層面來看,TypeOf 方法中主要涉及三塊操作,分別如下:
- 使用 unsafe.Pointer 方法獲取任意類型且可尋址的指針值。
- 利用 emptyInterface 類型進行強制的 interface 類型轉(zhuǎn)換。
- 調(diào)用 toType 方法轉(zhuǎn)換為可供外部使用的 Type 類型。
而這之中信息量最大的是 emptyInterface 結(jié)構(gòu)體中的 rtype 類型:
- type rtype struct {
- size uintptr
- ptrdata uintptr
- hash uint32
- tflag tflag
- align uint8
- fieldAlign uint8
- kind uint8
- equal func(unsafe.Pointer, unsafe.Pointer) bool
- gcdata *byte
- str nameOff
- ptrToThis typeOff
- }
在使用上最重要的是 rtype 類型,其實現(xiàn)了 Type 類型的所有接口方法,因此他可以直接作為 Type 類型返回。
而 Type 本質(zhì)上是一個接口實現(xiàn),其包含了獲取一個類型所必要的所有方法:
- type Type interface {
- // 適用于所有類型
- // 返回該類型內(nèi)存對齊后所占用的字節(jié)數(shù)
- Align() int
- // 僅作用于 strcut 類型
- // 返回該類型內(nèi)存對齊后所占用的字節(jié)數(shù)
- FieldAlign() int
- // 返回該類型的方法集中的第 i 個方法
- Method(int) Method
- // 根據(jù)方法名獲取對應方法集中的方法
- MethodByName(string) (Method, bool)
- // 返回該類型的方法集中導出的方法的數(shù)量。
- NumMethod() int
- // 返回該類型的名稱
- Name() string
- ...
- }
建議大致過一遍,了解清楚有哪些方法,再針對向看就好。
主體思想是給自己大腦建立一個索引,便于后續(xù)快速到 pkg.go.dev 上查詢即可。
reflect.ValueOf
演示程序:
- func main() {
- var x float64 = 3.4
- fmt.Println("value:", reflect.ValueOf(x))
- }
輸出結(jié)果:
- value: 3.4
從輸出結(jié)果中,可得知通過 reflect.ValueOf 成功獲取到了變量 x 的值為 3.4。與 reflect.TypeOf 形成一個相匹配,一個負責獲取類型,一個負責獲取值。
那么 reflect.ValueOf 是怎么獲取到值的呢,核心源碼如下:
- 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 方法中主要涉及如下幾個操作:
- 調(diào)用 escapes 讓變量 i 逃逸到堆上。
- 將變量 i 強制轉(zhuǎn)換為 emptyInterface 類型。
- 將所需的信息(其中包含值的具體類型和指針)組裝成 reflect.Value 類型后返回。
何時類型轉(zhuǎn)換
在調(diào)用 reflect 進行一系列反射行為時,Go 又是在什么時候進行的類型轉(zhuǎn)換呢?
畢竟我們傳入的是 float64,而函數(shù)如參數(shù)是 inetrface 類型。
查看匯編如下:
- $ go tool compile -S main.go
- ...
- 0x0058 00088 ($GOROOT/src/reflect/value.go:2817) LEAQ type.float64(SB), CX
- 0x005f 00095 ($GOROOT/src/reflect/value.go:2817) MOVQ CX, reflect.dummy+8(SB)
- 0x0066 00102 ($GOROOT/src/reflect/value.go:2817) PCDATA $0, $-2
- 0x0066 00102 ($GOROOT/src/reflect/value.go:2817) CMPL runtime.writeBarrier(SB), $0
- 0x006d 00109 ($GOROOT/src/reflect/value.go:2817) JNE 357
- 0x0073 00115 ($GOROOT/src/reflect/value.go:2817) MOVQ AX, reflect.dummy+16(SB)
- 0x007a 00122 ($GOROOT/src/reflect/value.go:2348) PCDATA $0, $-1
- 0x007a 00122 ($GOROOT/src/reflect/value.go:2348) MOVQ CX, reflect.i+64(SP)
- 0x007f 00127 ($GOROOT/src/reflect/value.go:2348) MOVQ AX, reflect.i+72(SP)
- ...
顯然,Go 語言會在編譯階段就會完成分析,且進行類型轉(zhuǎn)換。這樣子 reflect 真正所使用的就是 interface 類型了。
reflect.Set
演示程序:
- func main() {
- i := 2.33
- v := reflect.ValueOf(&i)
- v.Elem().SetFloat(6.66)
- log.Println("value: ", i)
- }
輸出結(jié)果:
- value: 6.66
從輸出結(jié)果中,我們可得知在調(diào)用 reflect.ValueOf 方法后,我們利用 SetFloat 方法進行了值變更。
核心的方法之一就是 Setter 相關的方法,我們可以一起看看其源碼是怎么實現(xiàn)的:
- func (v Value) Set(x Value) {
- v.mustBeAssignable()
- x.mustBeExported() // do not let unexported x leak
- var target unsafe.Pointer
- if v.kind() == Interface {
- target = v.ptr
- }
- x = x.assignTo("reflect.Set", v.typ, target)
- if x.flag&flagIndir != 0 {
- typedmemmove(v.typ, v.ptr, x.ptr)
- } else {
- *(*unsafe.Pointer)(v.ptr) = x.ptr
- }
- }
- 檢查反射對象及其字段是否可以被設置。
- 檢查反射對象及其字段是否導出(對外公開)。
- 調(diào)用 assignTo 方法創(chuàng)建一個新的反射對象并對原本的反射對象進行覆蓋。
- 根據(jù) assignTo 方法所返回的指針值,對當前反射對象的指針進行值的修改。
簡單來講就是,檢查是否可以設置,接著創(chuàng)建一個新的對象,最后對其修改。是一個非常標準的賦值流程。
反射三大定律
Go 語言中的反射,其歸根究底都是在實現(xiàn)三大定律:
- 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.
我們將針對這核心的三大定律進行介紹和說明,以此來理解 Go 反射里的各種方法是基于什么理念實現(xiàn)的。
第一定律
反射的第一定律是:“反射可以從接口值(interface)得到反射對象”。
示例代碼:
- func main() {
- var x float64 = 3.4
- fmt.Println("type:", reflect.TypeOf(x))
- }
輸出結(jié)果:
- type: float64
可能有讀者就迷糊了,我明明在代碼中傳入的變量 x,他的類型是 float64。怎么就成從接口值得到反射對象了。
其實不然,雖然在代碼中我們所傳入的變量基本類型是 float64,但是 reflect.TypeOf 方法入?yún)⑹?interface{},本質(zhì)上 Go 語言內(nèi)部對其是做了類型轉(zhuǎn)換的。這一塊會在后面會進一步展開說明。
第二定律
反射的第二定律是:“可以從反射對象得到接口值(interface)”。其與第一條定律是相反的定律,可以是互相補充了。
示例代碼:
- func main() {
- vo := reflect.ValueOf(3.4)
- vf := vo.Interface().(float64)
- log.Println("value:", vf)
- }
輸出結(jié)果:
- value: 3.4
可以看到在示例代碼中,變量 vo 已經(jīng)是反射對象,然后我們可以利用其所提供的的 Interface 方法獲取到接口值(interface),并最后強制轉(zhuǎn)換回我們原始的變量類型。
第三定律
反射的第三定律是:“要修改反射對象,該值必須可以修改”。第三條定律看上去與第一、第二條均無直接關聯(lián),但卻是必不可少的,因為反射在工程實踐中,目的一就是可以獲取到值和類型,其二就是要能夠修改他的值。
否則反射出來只能看,不能動,就會造成這個反射很雞肋。例如:應用程序中的配置熱更新,必然會涉及配置項相關的變量變動,大多會使用到反射來變動初始值。
示例代碼:
- func main() {
- i := 2.33
- v := reflect.ValueOf(&i)
- v.Elem().SetFloat(6.66)
- log.Println("value: ", i)
- }
輸出結(jié)果:
- value: 6.66
單從結(jié)果來看,變量 i 的值確實從 2.33 變成了 6.66,似乎非常完美。
但是單看代碼,似乎有些 “問題”,怎么設置一個反射值這么 ”麻煩“:
為什么必須傳入變量 i 的指針引用?
為什么變量 v 在設置前還需要 Elem 一下?
本叛逆的 Gophper 表示我就不這么設置,行不行呢,會不會出現(xiàn)什么問題:
- func main() {
- i := 2.33
- reflect.ValueOf(i).SetFloat(6.66)
- log.Println("value: ", i)
- }
報錯信息:
- panic: reflect: reflect.Value.SetFloat using unaddressable value
- goroutine 1 [running]:
- reflect.flag.mustBeAssignableSlow(0x8e)
- /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:259 +0x138
- reflect.flag.mustBeAssignable(...)
- /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:246
- reflect.Value.SetFloat(0x10b2980, 0xc00001a0b0, 0x8e, 0x401aa3d70a3d70a4)
- /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:1609 +0x37
- main.main()
- /Users/eddycjy/go-application/awesomeProject/main.go:10 +0xc5
根據(jù)上述提示可知,由于使用 “使用不可尋址的值”,因此示例程序無法正常的運作下去。并且這是一個 reflect 標準庫本身就加以防范了的硬性要求。
這么做的原因在于,Go 語言的函數(shù)調(diào)用的傳遞都是值拷貝的,因此若不傳指針引用,單純值傳遞,那么肯定是無法變動反射對象的源值的。因此 Go 標準庫就對其進行了邏輯判斷,避免出現(xiàn)問題。
因此期望變更反射對象的源值時,我們必須主動傳入對應變量的指針引用,并且調(diào)用 reflect 標準庫的 Elem 方法來獲取指針所指向的源變量,并且最后調(diào)用 Set 相關方法來進行設置。
總結(jié)
通過本文我們學習并了解了 Go 反射是如何使用,又是基于什么定律設計的。另外我們稍加關注,不難發(fā)現(xiàn) Go 的反射都是基于接口(interface)來實現(xiàn)的,更進一步來講,Go 語言中運行時的功能很多都是基于接口來實現(xiàn)的。
整體來講,Go 反射是圍繞著三者進行的,分別是 Type、Value 以及 Interface,三者相輔相成,而反射本質(zhì)上與 Interface 存在直接關系,Interface 這一塊的內(nèi)容我們也將在后續(xù)的文章進行進一步的剖析。