解密 Go 語言之反射 reflect
在所有的語言中,反射這一功能基本屬于必不可少的模塊。
雖說 “反射” 這個(gè)詞讓人根深蒂固,但更多的還是 WHY。反射到底是什么,反射又是基于什么法則實(shí)現(xiàn)的?

今天我們通過這篇文章來一一揭曉,以 Go 語言為例,了解反射到底為何物,其底層又是如何實(shí)現(xiàn)的。
反射是什么
在計(jì)算機(jī)學(xué)中,反射是指計(jì)算機(jī)程序在運(yùn)行時(shí)(runtime)可以訪問、檢測和修改它本身狀態(tài)或行為的一種能力。
用比喻來說,反射就是程序在運(yùn)行的時(shí)候能夠 “觀察” 并且修改自己的行為(來自維基百科)。

簡單來講就是,應(yīng)用程序能夠在運(yùn)行時(shí)觀察到變量的值,并且能夠修改他。
一個(gè)例子
最常見的 reflect 標(biāo)準(zhǔn)庫例子,如下:
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 個(gè)不同類型的值,分別是字符串、數(shù)字、閉包。
而在使用 interface{} 時(shí)常見于不知道入?yún)⒄呔唧w的基本類型是什么,那么我們就會(huì)用 interface{} 類型來做一個(gè)偽 “泛型”。
此時(shí)又會(huì)引出一個(gè)新的問題,既然入?yún)⑹?interface{},那么出參時(shí)呢?

Go 語言是強(qiáng)類型語言,入?yún)⑹?interface{},出參也肯定是跑不了的,因此必然離不開類型的判斷,這時(shí)候就要用到反射,也就是 reflect 標(biāo)準(zhǔn)庫。反射過后又再進(jìn)行 (type) 的類型斷言。
這就是我們?cè)诰帉懗绦驎r(shí)最常遇見的一個(gè)反射使用場景。
Go reflect
reflect 標(biāo)準(zhǔn)庫中,最核心的莫過于 reflect.Type 和 reflect.Value 類型。而在反射中所使用的方法都圍繞著這兩者進(jìn)行,其方法主要含義如下:

TypeOf方法:用于提取入?yún)⒅档?strong>類型信息。ValueOf方法:用于提取存儲(chǔ)的變量的值信息。
reflect.TypeOf
演示程序:
func?main()?{
?blog?:=?Blog{"煎魚"}
?typeof?:=?reflect.TypeOf(blog)
?fmt.Println(typeof.String())
}
輸出結(jié)果:
main.Blog
從輸出結(jié)果中,可得出 reflect.TypeOf 成功解析出 blog 變量的類型是 main.Blog,也就是連 package 都知道了。
通過人識(shí)別的角度來看似乎很正常,但程序就不是這樣了。他是怎么知道 “他” 是哪個(gè) package 下的什么呢?
我們一起追一下源碼看看:
func?TypeOf(i?interface{})?Type?{
?eface?:=?*(*emptyInterface)(unsafe.Pointer(&i))
?return?toType(eface.typ)
}
從源碼層面來看,TypeOf 方法中主要涉及三塊操作,分別如下:
使用
unsafe.Pointer方法獲取任意類型且可尋址的指針值。利用
emptyInterface類型進(jìn)行強(qiáng)制的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 類型,其實(shí)現(xiàn)了 Type 類型的所有接口方法,因此他可以直接作為 Type 類型返回。
而 Type 本質(zhì)上是一個(gè)接口實(shí)現(xiàn),其包含了獲取一個(gè)類型所必要的所有方法:
type?Type?interface?{
?//?適用于所有類型
?//?返回該類型內(nèi)存對(duì)齊后所占用的字節(jié)數(shù)
?Align()?int
?//?僅作用于?strcut?類型
?//?返回該類型內(nèi)存對(duì)齊后所占用的字節(jié)數(shù)
?FieldAlign()?int
?//?返回該類型的方法集中的第?i?個(gè)方法
?Method(int)?Method
?//?根據(jù)方法名獲取對(duì)應(yīng)方法集中的方法
?MethodByName(string)?(Method,?bool)
?//?返回該類型的方法集中導(dǎo)出的方法的數(shù)量。
?NumMethod()?int
?//?返回該類型的名稱
?Name()?string
?...
}
建議大致過一遍,了解清楚有哪些方法,再針對(duì)向看就好。
主體思想是給自己大腦建立一個(gè)索引,便于后續(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 形成一個(gè)相匹配,一個(gè)負(fù)責(zé)獲取類型,一個(gè)負(fù)責(zé)獲取值。
那么 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 方法中主要涉及如下幾個(gè)操作:
調(diào)用
escapes讓變量i逃逸到堆上。將變量
i強(qiáng)制轉(zhuǎn)換為emptyInterface類型。將所需的信息(其中包含值的具體類型和指針)組裝成
reflect.Value類型后返回。
何時(shí)類型轉(zhuǎn)換
在調(diào)用 reflect 進(jìn)行一系列反射行為時(shí),Go 又是在什么時(shí)候進(jìn)行的類型轉(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 語言會(huì)在編譯階段就會(huì)完成分析,且進(jìn)行類型轉(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 方法進(jìn)行了值變更。
核心的方法之一就是 Setter 相關(guān)的方法,我們可以一起看看其源碼是怎么實(shí)現(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
?}
}
檢查反射對(duì)象及其字段是否可以被設(shè)置。
檢查反射對(duì)象及其字段是否導(dǎo)出(對(duì)外公開)。
調(diào)用
assignTo方法創(chuàng)建一個(gè)新的反射對(duì)象并對(duì)原本的反射對(duì)象進(jìn)行覆蓋。根據(jù)
assignTo方法所返回的指針值,對(duì)當(dāng)前反射對(duì)象的指針進(jìn)行值的修改。
簡單來講就是,檢查是否可以設(shè)置,接著創(chuàng)建一個(gè)新的對(duì)象,最后對(duì)其修改。是一個(gè)非常標(biāo)準(zhǔn)的賦值流程。
反射三大定律
Go 語言中的反射,其歸根究底都是在實(shí)現(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.
我們將針對(duì)這核心的三大定律進(jìn)行介紹和說明,以此來理解 Go 反射里的各種方法是基于什么理念實(shí)現(xiàn)的。
第一定律
反射的第一定律是:“反射可以從接口值(interface)得到反射對(duì)象”。
示例代碼:
func?main()?{
?var?x?float64?=?3.4
?fmt.Println("type:",?reflect.TypeOf(x))
}
輸出結(jié)果:
type:?float64
可能有讀者就迷糊了,我明明在代碼中傳入的變量 x,他的類型是 float64。怎么就成從接口值得到反射對(duì)象了。
其實(shí)不然,雖然在代碼中我們所傳入的變量基本類型是 float64,但是 reflect.TypeOf 方法入?yún)⑹?interface{},本質(zhì)上 Go 語言內(nèi)部對(duì)其是做了類型轉(zhuǎn)換的。這一塊會(huì)在后面會(huì)進(jìn)一步展開說明。
第二定律
反射的第二定律是:“可以從反射對(duì)象得到接口值(interface)”。其與第一條定律是相反的定律,可以是互相補(bǔ)充了。
示例代碼:
func?main()?{
?vo?:=?reflect.ValueOf(3.4)
?vf?:=?vo.Interface().(float64)
?log.Println("value:",?vf)
}
輸出結(jié)果:
value:?3.4
可以看到在示例代碼中,變量 vo 已經(jīng)是反射對(duì)象,然后我們可以利用其所提供的的 Interface 方法獲取到接口值(interface),并最后強(qiáng)制轉(zhuǎn)換回我們?cè)嫉淖兞款愋汀?/p>
第三定律
反射的第三定律是:“要修改反射對(duì)象,該值必須可以修改”。第三條定律看上去與第一、第二條均無直接關(guān)聯(lián),但卻是必不可少的,因?yàn)榉瓷湓诠こ虒?shí)踐中,目的一就是可以獲取到值和類型,其二就是要能夠修改他的值。
否則反射出來只能看,不能動(dòng),就會(huì)造成這個(gè)反射很雞肋。例如:應(yīng)用程序中的配置熱更新,必然會(huì)涉及配置項(xiàng)相關(guān)的變量變動(dòng),大多會(huì)使用到反射來變動(dòng)初始值。
示例代碼:
func?main()?{
?i?:=?2.33
?v?:=?reflect.ValueOf(&i)
?v.Elem().SetFloat(6.66)
?log.Println("value:?",?i)
}
輸出結(jié)果:
value:??6.66
單從結(jié)果來看,變量 i 的值確實(shí)從 2.33 變成了 6.66,似乎非常完美。
但是單看代碼,似乎有些 “問題”,怎么設(shè)置一個(gè)反射值這么 ”麻煩“:
為什么必須傳入變量
i的指針引用?為什么變量
v在設(shè)置前還需要Elem一下?
本叛逆的 Gophper 表示我就不這么設(shè)置,行不行呢,會(huì)不會(huì)出現(xiàn)什么問題:
func?main()?{
?i?:=?2.33
?reflect.ValueOf(i).SetFloat(6.66)
?log.Println("value:?",?i)
}
報(bào)錯(cuò)信息:
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ù)上述提示可知,由于使用 “使用不可尋址的值”,因此示例程序無法正常的運(yùn)作下去。并且這是一個(gè) reflect 標(biāo)準(zhǔn)庫本身就加以防范了的硬性要求。
這么做的原因在于,Go 語言的函數(shù)調(diào)用的傳遞都是值拷貝的,因此若不傳指針引用,單純值傳遞,那么肯定是無法變動(dòng)反射對(duì)象的源值的。因此 Go 標(biāo)準(zhǔn)庫就對(duì)其進(jìn)行了邏輯判斷,避免出現(xiàn)問題。
因此期望變更反射對(duì)象的源值時(shí),我們必須主動(dòng)傳入對(duì)應(yīng)變量的指針引用,并且調(diào)用 reflect 標(biāo)準(zhǔn)庫的 Elem 方法來獲取指針?biāo)赶虻脑醋兞浚⑶易詈笳{(diào)用 Set 相關(guān)方法來進(jìn)行設(shè)置。
總結(jié)
通過本文我們學(xué)習(xí)并了解了 Go 反射是如何使用,又是基于什么定律設(shè)計(jì)的。另外我們稍加關(guān)注,不難發(fā)現(xiàn) Go 的反射都是基于接口(interface)來實(shí)現(xiàn)的,更進(jìn)一步來講,Go 語言中運(yùn)行時(shí)的功能很多都是基于接口來實(shí)現(xiàn)的。
整體來講,Go 反射是圍繞著三者進(jìn)行的,分別是 Type、Value 以及 Interface,三者相輔相成,而反射本質(zhì)上與 Interface 存在直接關(guān)系,Interface 這一塊的內(nèi)容我們也將在后續(xù)的文章進(jìn)行進(jìn)一步的剖析。

???
