反射是 Go 語言比較重要的一個(gè)特性之一,雖然在大多數(shù)的應(yīng)用和服務(wù)中并不常見,但是很多框架都依賴 Go 語言的反射機(jī)制實(shí)現(xiàn)一些動(dòng)態(tài)的功能。作為一門靜態(tài)語言,Golang 在設(shè)計(jì)上都非常簡(jiǎn)潔,所以在語法上其實(shí)并沒有較強(qiáng)的表達(dá)能力,但是 Go 語言為我們提供的 reflect 包提供的動(dòng)態(tài)特性卻能夠彌補(bǔ)它在語法上的一些劣勢(shì)。
reflect 實(shí)現(xiàn)了運(yùn)行時(shí)的反射能力,能夠讓 Golang 的程序操作不同類型的對(duì)象,我們可以使用包中的函數(shù) TypeOf 從靜態(tài)類型 interface{} 中獲取動(dòng)態(tài)類型信息并通過 ValueOf 獲取數(shù)據(jù)的運(yùn)行時(shí)表示,通過這兩個(gè)函數(shù)和包中的其他工具我們就可以得到更強(qiáng)大的表達(dá)能力。
概述
在具體介紹反射包的實(shí)現(xiàn)原理之前,我們先要對(duì) Go 語言的反射有一些比較簡(jiǎn)單的理解,首先 reflect 中有兩對(duì)非常重要的函數(shù)和類型,我們?cè)谏厦嬉呀?jīng)介紹過其中的兩個(gè)函數(shù) TypeOf 和 ValueOf,另外兩個(gè)類型是 Type 和 Value,它們與函數(shù)是一一對(duì)應(yīng)的關(guān)系:

類型 Type 是 Golang 反射包中定義的一個(gè)接口,我們可以使用 TypeOf 函數(shù)獲取任意值的變量的的類型,我們能從這個(gè)接口中看到非常多有趣的方法,MethodByName 可以獲取當(dāng)前類型對(duì)應(yīng)方法的引用、Implements 可以判斷當(dāng)前類型是否實(shí)現(xiàn)了某個(gè)接口:
復(fù)制代碼
type Type interface { Align() int FieldAlign() int Method(int) Method MethodByName(string) (Method, bool) NumMethod() int Name() string PkgPath() string Size() uintptr String() string Kind() Kind Implements(u Type) bool ...}
反射包中 Value 的類型卻與 Type 不同,Type 是一個(gè)接口類型,但是 Value 在 reflect 包中的定義是一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體沒有任何對(duì)外暴露的成員變量,但是卻提供了很多方法讓我們獲取或者寫入 Value 結(jié)構(gòu)體中存儲(chǔ)的數(shù)據(jù):
復(fù)制代碼
type Value struct { // contains filtered or unexported fields} func (v Value) Addr() Valuefunc (v Value) Bool() boolfunc (v Value) Bytes() []bytefunc (v Value) Float() float64...
反射包中的所有方法基本都是圍繞著 Type 和 Value 這兩個(gè)對(duì)外暴露的類型設(shè)計(jì)的,我們通過 TypeOf、ValueOf 方法就可以將一個(gè)普通的變量轉(zhuǎn)換成『反射』包中提供的 Type 和 Value,使用反射提供的方法對(duì)這些類型進(jìn)行復(fù)雜的操作。
反射法則
運(yùn)行時(shí)反射是程序在運(yùn)行期間檢查其自身結(jié)構(gòu)的一種方式,它是 元編程 的一種,但是它帶來的靈活性也是一把雙刃劍,過量的使用反射會(huì)使我們的程序邏輯變得難以理解并且運(yùn)行緩慢,我們?cè)谶@一節(jié)中就會(huì)介紹 Go 語言反射的三大法則,這能夠幫助我們更好地理解反射的作用。
- 從接口值可反射出反射對(duì)象;
- 從反射對(duì)象可反射出接口值;
- 要修改反射對(duì)象,其值必須可設(shè)置;
第一法則
反射的第一條法則就是,我們能夠?qū)?Go 語言中的接口類型變量轉(zhuǎn)換成反射對(duì)象,上面提到的reflect.TypeOf 和 reflect.ValueOf 就是完成這個(gè)轉(zhuǎn)換的兩個(gè)最重要方法,如果我們認(rèn)為 Go 語言中的類型和反射類型是兩個(gè)不同『世界』的話,那么這兩個(gè)方法就是連接這兩個(gè)世界的橋梁。

我們通過以下例子簡(jiǎn)單介紹這兩個(gè)方法的作用,其中 TypeOf 獲取了變量 author 的類型也就是 string 而 ValueOf 獲取了變量的值 draven,如果我們知道了一個(gè)變量的類型和值,那么也就意味著我們知道了關(guān)于這個(gè)變量的全部信息。
復(fù)制代碼
package main import ( "fmt" "reflect") func main() { author := "draven" fmt.Println("TypeOf author:", reflect.TypeOf(author)) fmt.Println("ValueOf author:", reflect.ValueOf(author))} $ go run main.goTypeOf author: stringValueOf author: draven
從變量的類型上我們可以獲當(dāng)前類型能夠執(zhí)行的方法 Method 以及當(dāng)前類型實(shí)現(xiàn)的接口等信息;
- 對(duì)于結(jié)構(gòu)體,可以獲取字段的數(shù)量并通過下標(biāo)和字段名獲取字段 StructField;
- 對(duì)于哈希表,可以獲取哈希表的 Key 類型;
- 對(duì)于函數(shù)或方法,可以獲得入?yún)⒑头祷刂档念愋停?/li>
- …
總而言之,使用 TypeOf 和 ValueOf 能夠?qū)?Go 語言中的變量轉(zhuǎn)換成反射對(duì)象,在這時(shí)我們能夠獲得幾乎一切跟當(dāng)前類型相關(guān)數(shù)據(jù)和操作,然后就可以用這些運(yùn)行時(shí)獲取的結(jié)構(gòu)動(dòng)態(tài)的執(zhí)行一些方法。
很多讀者可能都會(huì)對(duì)這個(gè)副標(biāo)題產(chǎn)生困惑,為什么是從接口到反射對(duì)象,如果直接調(diào)用 reflect.ValueOf(1),看起來是從基本類型 int 到反射類型,但是 TypeOf 和 ValueOf 兩個(gè)方法的入?yún)⑵鋵?shí)是 interface{} 類型。
我們?cè)谥耙呀?jīng)在 函數(shù)調(diào)用 一節(jié)中介紹過,Go 語言的函數(shù)調(diào)用都是值傳遞的,變量會(huì)在方法調(diào)用前進(jìn)行類型轉(zhuǎn)換,也就是 int 類型的基本變量會(huì)被轉(zhuǎn)換成 interface{} 類型,這也就是第一條法則介紹的是從接口到反射對(duì)象。
第二法則
我們既然能夠?qū)⒔涌陬愋偷淖兞哭D(zhuǎn)換成反射對(duì)象類型,那么也需要一些其他方法將反射對(duì)象還原成成接口類型的變量, reflect 中的 Interface 方法就能完成這項(xiàng)工作:

然而調(diào)用 Interface 方法我們也只能獲得 interface{} 類型的接口變量,如果想要將其還原成原本的類型還需要經(jīng)過一次強(qiáng)制的類型轉(zhuǎn)換,如下所示:
復(fù)制代碼
v := reflect.ValueOf(1)v.Interface{}.(int)
這個(gè)過程就像從接口值到反射對(duì)象的鏡面過程一樣,從接口值到反射對(duì)象需要經(jīng)過從基本類型到接口類型的類型轉(zhuǎn)換和從接口類型到反射對(duì)象類型的轉(zhuǎn)換,反過來的話,所有的反射對(duì)象也都需要先轉(zhuǎn)換成接口類型,再通過強(qiáng)制類型轉(zhuǎn)換變成原始類型:

當(dāng)然不是所有的變量都需要類型轉(zhuǎn)換這一過程,如果本身就是 interface{} 類型的,那么它其實(shí)并不需要經(jīng)過類型轉(zhuǎn)換,對(duì)于大多數(shù)的變量來說,類型轉(zhuǎn)換這一過程很多時(shí)候都是隱式發(fā)生的,只有在我們需要將反射對(duì)象轉(zhuǎn)換回基本類型時(shí)才需要做顯示的轉(zhuǎn)換操作。
第三法則
Go 語言反射的最后一條法則是與值是否可以被更改相關(guān)的,如果我們想要更新一個(gè) reflect.Value,那么它持有的值一定是可以被更新的,假設(shè)我們有以下代碼:
復(fù)制代碼
func main() { i := 1 v := reflect.ValueOf(i) v.SetInt(10) fmt.Println(i)} $ go run reflect.gopanic: reflect: reflect.flag.mustBeAssignable using unaddressable value goroutine 1 [running]:reflect.flag.mustBeAssignableSlow(0x82, 0x1014c0) /usr/local/go/src/reflect/value.go:247 +0x180reflect.flag.mustBeAssignable(...) /usr/local/go/src/reflect/value.go:234reflect.Value.SetInt(0x100dc0, 0x414020, 0x82, 0x1840, 0xa, 0x0) /usr/local/go/src/reflect/value.go:1606 +0x40main.main() /tmp/sandbox590309925/prog.go:11 +0xe0
運(yùn)行上述代碼時(shí)會(huì)導(dǎo)致程序 panic 并報(bào)出 reflect: reflect.flag.mustBeAssignable using unaddressable value 錯(cuò)誤,仔細(xì)想一下其實(shí)能夠發(fā)現(xiàn)出錯(cuò)的原因,Go 語言的 函數(shù)調(diào)用 都是傳值的,所以我們得到的反射對(duì)象其實(shí)跟最開始的變量沒有任何關(guān)系,沒有任何變量持有復(fù)制出來的值,所以直接對(duì)它修改會(huì)導(dǎo)致崩潰。
想要修改原有的變量我們只能通過如下所示的方法,首先通過 reflect.ValueOf 獲取變量指針,然后通過 Elem 方法獲取指針指向的變量并調(diào)用 SetInt 方法更新變量的值:
復(fù)制代碼
func main() { i := 1 v := reflect.ValueOf(&i) v.Elem().SetInt(10) fmt.Println(i)} $ go run reflect.go10
這種獲取指針對(duì)應(yīng)的 reflect.Value 并通過 Elem 方法迂回的方式就能夠獲取到可以被設(shè)置的變量,這一復(fù)雜的過程主要也是因?yàn)?Go 語言的函數(shù)調(diào)用都是值傳遞的,我們可以將上述代碼理解成:
復(fù)制代碼
func main() { i := 1 v := &i *v = 10}
如果不能直接操作 i 變量修改其持有的值,我們就只能獲取 i 變量所在地址并使用 *v 修改所在地址中存儲(chǔ)的整數(shù)。
實(shí)現(xiàn)原理
我們?cè)谏厦娴牟糠忠呀?jīng)對(duì) Go 語言中反射的三大法則進(jìn)行了介紹,對(duì)于接口值和反射對(duì)象互相轉(zhuǎn)換的操作和過程都有了一定的了解,接下來我們就深入研究反射的實(shí)現(xiàn)原理,分析 reflect 包提供的方法是如何獲取接口值對(duì)應(yīng)的反射類型和值、判斷協(xié)議的實(shí)現(xiàn)以及方法調(diào)用的過程。
類型和值
Golang 的 interface{} 類型在語言內(nèi)部都是通過 emptyInterface 這個(gè)結(jié)體來表示的,其中包含一個(gè) rtype 字段用于表示變量的類型以及一個(gè) word 字段指向內(nèi)部封裝的數(shù)據(jù):
復(fù)制代碼
type emptyInterface struct { typ *rtype word unsafe.Pointer}
用于獲取變量類型的 TypeOf 函數(shù)就是將傳入的 i 變量強(qiáng)制轉(zhuǎn)換成 emptyInterface 類型并獲取其中存儲(chǔ)的類型信息 rtype:
復(fù)制代碼
func TypeOf(i interface{}) Type { eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ)} func toType(t *rtype) Type { if t == nil { return nil } return t}
rtype 就是一個(gè)實(shí)現(xiàn)了 Type 接口的接口體,我們能在 reflect 包中找到如下所示的 Name 方法幫助我們獲取當(dāng)前類型的名稱等信息:
復(fù)制代碼
func (t *rtype) String() string { s := t.nameOff(t.str).name() if t.tflag&tflagExtraStar != 0 { return s[1:] } return s}
TypeOf 函數(shù)的實(shí)現(xiàn)原理其實(shí)并不復(fù)雜,它只是將一個(gè) interface{} 變量轉(zhuǎn)換成了內(nèi)部的 emptyInterface 表示,然后從中獲取相應(yīng)的類型信息。
用于獲取接口值 Value 的函數(shù) ValueOf 實(shí)現(xiàn)也非常簡(jiǎn)單,在該函數(shù)中我們先調(diào)用了 escapes 函數(shù)保證當(dāng)前值逃逸到堆上,然后通過 unpackEface 方法從接口中獲取 Value 結(jié)構(gòu)體:
復(fù)制代碼
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}}
unpackEface 函數(shù)會(huì)將傳入的接口 interface{} 轉(zhuǎn)換成 emptyInterface 結(jié)構(gòu)體然后將其中表示接口值類型、指針以及值的類型包裝成 Value 結(jié)構(gòu)體并返回。
TypeOf 和 ValueOf 兩個(gè)方法的實(shí)現(xiàn)其實(shí)都非常簡(jiǎn)單,從一個(gè) Go 語言的基本變量中獲取反射對(duì)象以及類型的過程中,TypeOf 和 ValueOf 兩個(gè)方法的執(zhí)行過程并不是特別的復(fù)雜,我們還需要注意基本變量到接口值的轉(zhuǎn)換過程:
復(fù)制代碼
package main import ( "reflect") func main() { i := 20 _ = reflect.TypeOf(i)} $ go build -gcflags="-S -N" main.go...MOVQ $20, ""..autotmp_20+56(SP) // autotmp = 20LEAQ type.int(SB), AX // AX = type.int(SB)MOVQ AX, ""..autotmp_19+280(SP) // autotmp_19+280(SP) = type.int(SB)LEAQ ""..autotmp_20+56(SP), CX // CX = 20MOVQ CX, ""..autotmp_19+288(SP) // autotmp_19+288(SP) = 20...
我們使用 -S -N 編譯指令編譯了上述代碼,從這段截取的匯編語言中我們可以發(fā)現(xiàn),在函數(shù)調(diào)用之前其實(shí)發(fā)生了類型轉(zhuǎn)換,我們將 int 類型的變量轉(zhuǎn)換成了占用 16 字節(jié) autotmp_19+280(SP) ~ autotmp_19+288(SP) 的 interface{} 結(jié)構(gòu)體,兩個(gè) LEAQ 指令分別獲取了類型的指針 type.int(SB) 以及變量 i 所在的地址。
總的來說,在 Go 語言的編譯期間我們就完成了類型轉(zhuǎn)換的工作,將變量的類型和值轉(zhuǎn)換成了 interface{} 等待運(yùn)行期間使用 reflect 包獲取其中存儲(chǔ)的信息。
更新變量
當(dāng)我們想要更新一個(gè) reflect.Value 時(shí),就需要調(diào)用 Set 方法更新反射對(duì)象,該方法會(huì)調(diào)用 mustBeAssignable 和 mustBeExported 分別檢查當(dāng)前反射對(duì)象是否是可以被設(shè)置的和對(duì)外暴露的公開字段:
復(fù)制代碼
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 }}Set
Set 方法中會(huì)調(diào)用 assignTo,該方法會(huì)返回一個(gè)新的 reflect.Value 反射對(duì)象,我們可以將反射對(duì)象的指針直接拷貝到被設(shè)置的反射變量上:
復(fù)制代碼
func (v Value) assignTo(context string, dst *rtype, target unsafe.Pointer) Value { if v.flag&flagMethod != 0 { v = makeMethodValue(context, v) } switch { case directlyAssignable(dst, v.typ): fl := v.flag&(flagAddr|flagIndir) | v.flag.ro() fl |= flag(dst.Kind()) return Value{dst, v.ptr, fl} case implements(dst, v.typ): if target == nil { target = unsafe_New(dst) } if v.Kind() == Interface && v.IsNil() { return Value{dst, nil, flag(Interface)} } x := valueInterface(v, false) if dst.NumMethod() == 0 { *(*interface{})(target) = x } else { ifaceE2I(dst, x, target) } return Value{dst, target, flagIndir | flag(Interface)} } panic(context + ": value of type " + v.typ.String() + " is not assignable to type " + dst.String())}
assignTo 會(huì)根據(jù)當(dāng)前和被設(shè)置的反射對(duì)象類型創(chuàng)建一個(gè)新的 Value 結(jié)構(gòu)體,當(dāng)兩個(gè)反射對(duì)象的類型是可以被直接替換時(shí),就會(huì)直接將目標(biāo)反射對(duì)象返回;如果當(dāng)前反射對(duì)象是接口并且目標(biāo)對(duì)象實(shí)現(xiàn)了接口,就會(huì)將目標(biāo)對(duì)象簡(jiǎn)單包裝成接口值,上述方法返回反射對(duì)象的 ptr 最終會(huì)覆蓋當(dāng)前反射對(duì)象中存儲(chǔ)的值。
實(shí)現(xiàn)協(xié)議
reflect 包還為我們提供了 Implements 方法用于判斷某些類型是否遵循協(xié)議實(shí)現(xiàn)了全部的方法,在 Go 語言中想要獲取結(jié)構(gòu)體的類型還是比較容易的,但是想要獲得接口的類型就需要比較黑魔法的方式:
復(fù)制代碼
reflect.TypeOf((*<interface>)(nil)).Elem()
只有通過上述方式才能獲得一個(gè)接口類型的反射對(duì)象,假設(shè)我們有以下代碼,我們需要判斷 CustomError 是否實(shí)現(xiàn)了 Go 語言標(biāo)準(zhǔn)庫(kù)中的 error 協(xié)議:
復(fù)制代碼
type CustomError struct{} func (*CustomError) Error() string { return ""} func main() { typeOfError := reflect.TypeOf((*error)(nil)).Elem() customErrorPtr := reflect.TypeOf(&CustomError{}) customError := reflect.TypeOf(CustomError{}) fmt.Println(customErrorPtr.Implements(typeOfError)) // #=> true fmt.Println(customError.Implements(typeOfError)) // #=> false}
運(yùn)行上述代碼我們會(huì)發(fā)現(xiàn) CustomError 類型并沒有實(shí)現(xiàn) error 接口,而 *CustomError 指針類型卻實(shí)現(xiàn)了接口,這其實(shí)也比較好理解,我們?cè)?接口 一節(jié)中也介紹過可以使用結(jié)構(gòu)體和指針兩種不同的類型實(shí)現(xiàn)接口。
復(fù)制代碼
func (t *rtype) Implements(u Type) bool { if u == nil { panic("reflect: nil type passed to Type.Implements") } if u.Kind() != Interface { panic("reflect: non-interface type passed to Type.Implements") } return implements(u.(*rtype), t)}
Implements 方法會(huì)檢查傳入的類型是不是接口,如果不是接口或者是空值就會(huì)直接 panic 中止當(dāng)前程序,否則就會(huì)調(diào)用私有的函數(shù) implements 判斷類型之間是否有實(shí)現(xiàn)關(guān)系:
復(fù)制代碼
func implements(T, V *rtype) bool { t := (*interfaceType)(unsafe.Pointer(T)) if len(t.methods) == 0 { return true } // ... v := V.uncommon() i := 0 vmethods := v.methods() for j := 0; j < int(v.mcount); j++ { tm := &t.methods[i] tmName := t.nameOff(tm.name) vm := vmethods[j] vmName := V.nameOff(vm.name) if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) { if i++; i >= len(t.methods) { return true } } } return false}
如果接口中不包含任何方法,也就意味著這是一個(gè)空的 interface{},任意的類型都可以實(shí)現(xiàn)該協(xié)議,所以就會(huì)直接返回 true。

在其他情況下,由于方法是按照一定順序排列的,implements 中就會(huì)維護(hù)兩個(gè)用于遍歷接口和類型方法的索引 i 和 j,所以整個(gè)過程的實(shí)現(xiàn)復(fù)雜度是 O(n+m),最多只會(huì)進(jìn)行 n + m 次數(shù)的比較,不會(huì)出現(xiàn)次方級(jí)別的復(fù)雜度。
方法調(diào)用
作為一門靜態(tài)語言,如果我們想要通過 reflect 包利用反射在運(yùn)行期間執(zhí)行方法并不是一件容易的事情,下面的代碼就使用了反射來執(zhí)行 Add(0, 1) 這一表達(dá)式:
復(fù)制代碼
func Add(a, b int) int { return a + b } func main() { v := reflect.ValueOf(Add) if v.Kind() != reflect.Func { return } t := v.Type() argv := make([]reflect.Value, t.NumIn()) for i := range argv { if t.In(i).Kind() != reflect.Int { return } argv[i] = reflect.ValueOf(i) } result := v.Call(argv) if len(result) != 1 || result[0].Kind() != reflect.Int { return } fmt.Println(result[0].Int()) // #=> 1}
- 通過 reflect.ValueOf 獲取函數(shù) Add 對(duì)應(yīng)的反射對(duì)象;
- 根據(jù)反射對(duì)象 NumIn 方法返回的參數(shù)個(gè)數(shù)創(chuàng)建 argv 數(shù)組;
- 多次調(diào)用 reflect.Value 逐一設(shè)置 argv 數(shù)組中的各個(gè)參數(shù);
- 調(diào)用反射對(duì)象 Add 的 Call 方法并傳入?yún)?shù)列表;
- 獲取返回值數(shù)組、驗(yàn)證數(shù)組的長(zhǎng)度以及類型并打印其中的數(shù)據(jù);
使用反射來調(diào)用方法非常復(fù)雜,原本只需要一行代碼就能完成的工作,現(xiàn)在需要 10 多行代碼才能完成,但是這也是在靜態(tài)語言中使用這種動(dòng)態(tài)特性需要付出的成本,理解這個(gè)調(diào)用過程能夠幫助我們深入理解 Go 語言函數(shù)和方法調(diào)用的原理。
復(fù)制代碼
func (v Value) Call(in []Value) []Value { v.mustBe(Func) v.mustBeExported() return v.call("Call", in)}
Call 作為反射包運(yùn)行時(shí)調(diào)用方法的入口,通過兩個(gè) MustBe 方法保證了當(dāng)前反射對(duì)象的類型和可見性,隨后調(diào)用 call 方法完成運(yùn)行時(shí)方法調(diào)用的過程,這個(gè)過程會(huì)被分成以下的幾個(gè)部分:
- 檢查輸入?yún)?shù)的合法性以及類型等信息;
- 將傳入的 reflect.Value 參數(shù)數(shù)組設(shè)置到棧上;
- 通過函數(shù)指針和輸入?yún)?shù)調(diào)用函數(shù);
- 從棧上獲取函數(shù)的返回值;
我們將按照上面的順序依次詳細(xì)介紹使用 reflect 進(jìn)行函數(shù)調(diào)用的幾個(gè)過程。
參數(shù)檢查
參數(shù)檢查是通過反射調(diào)用方法的第一步,在參數(shù)檢查期間我們會(huì)從反射對(duì)象中取出當(dāng)前的函數(shù)指針 unsafe.Pointer,如果待執(zhí)行的函數(shù)是方法,就會(huì)通過 methodReceiver 函數(shù)獲取方法的接受者和函數(shù)指針。
復(fù)制代碼
func (v Value) call(op string, in []Value) []Value { t := (*funcType)(unsafe.Pointer(v.typ)) var ( fn unsafe.Pointer rcvr Value rcvrtype *rtype ) if v.flag&flagMethod != 0 { rcvr = v rcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift) } else if v.flag&flagIndir != 0 { fn = *(*unsafe.Pointer)(v.ptr) } else { fn = v.ptr } n := t.NumIn() if len(in) < n { panic("reflect: Call with too few input arguments") } if len(in) > n { panic("reflect: Call with too many input arguments") } for i := 0; i < n; i++ { if xt, targ := in[i].Type(), t.In(i); !xt.AssignableTo(targ) { panic("reflect: " + op + " using " + xt.String() + " as type " + targ.String()) } } nin := len(in) if nin != t.NumIn() { panic("reflect.Value.Call: wrong argument count") }
除此之外,在參數(shù)檢查的過程中我們還會(huì)檢查當(dāng)前傳入?yún)?shù)的個(gè)數(shù)以及所有參數(shù)的類型是否能被傳入該函數(shù)中,任何參數(shù)不匹配的問題都會(huì)導(dǎo)致當(dāng)前函數(shù)直接 panic 并中止整個(gè)程序。
準(zhǔn)備參數(shù)
當(dāng)我們已經(jīng)對(duì)當(dāng)前方法的參數(shù)驗(yàn)證完成之后,就會(huì)進(jìn)入函數(shù)調(diào)用的下一個(gè)階段,為函數(shù)調(diào)用準(zhǔn)備參數(shù),在前面的章節(jié) 函數(shù)調(diào)用 中我們已經(jīng)介紹過 Go 語言的函數(shù)調(diào)用的慣例,所有的參數(shù)都會(huì)被依次放置到堆棧上。
復(fù)制代碼
nout := t.NumOut() frametype, _, retOffset, _, framePool := funcLayout(t, rcvrtype) var args unsafe.Pointer if nout == 0 { args = framePool.Get().(unsafe.Pointer) } else { args = unsafe_New(frametype) } off := uintptr(0) if rcvrtype != nil { storeRcvr(rcvr, args) off = ptrSize } for i, v := range in { targ := t.In(i).(*rtype) a := uintptr(targ.align) off = (off + a - 1) &^ (a - 1) n := targ.size if n == 0 { v.assignTo("reflect.Value.Call", targ, nil) continue } addr := add(args, off, "n > 0") v = v.assignTo("reflect.Value.Call", targ, addr) if v.flag&flagIndir != 0 { typedmemmove(targ, addr, v.ptr) } else { *(*unsafe.Pointer)(addr) = v.ptr } off += n }
- 通過 funcLayout 函數(shù)計(jì)算當(dāng)前函數(shù)需要的參數(shù)和返回值的堆棧布局,也就是每一個(gè)參數(shù)和返回值所占的空間大小;
- 如果當(dāng)前函數(shù)有返回值,需要為當(dāng)前函數(shù)的參數(shù)和返回值分配一片內(nèi)存空間 args;
- 如果當(dāng)前函數(shù)是方法,需要向?qū)⒎椒ǖ慕邮苷呖截惖?args 這片內(nèi)存中;
- 將所有函數(shù)的參數(shù)按照順序依次拷貝到對(duì)應(yīng) args 內(nèi)存中使用 funcLayout 返回的參數(shù)計(jì)算參數(shù)在內(nèi)存中的位置;通過 typedmemmove 或者尋址的放置拷貝參數(shù);
準(zhǔn)備參數(shù)的過程其實(shí)就是計(jì)算各個(gè)參數(shù)和返回值占用的內(nèi)存空間,并將所有的參數(shù)都拷貝內(nèi)存空間對(duì)應(yīng)的位置上。
調(diào)用函數(shù)
準(zhǔn)備好調(diào)用函數(shù)需要的全部參數(shù)之后,就會(huì)通過以下的表達(dá)式開始方法的調(diào)用了,我們會(huì)向該函數(shù)中傳入棧類型、函數(shù)指針、參數(shù)和返回值的內(nèi)存空間、棧的大小以及返回值的偏移量:
復(fù)制代碼
call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))
這個(gè)函數(shù)實(shí)際上并不存在,它會(huì)在編譯期間被鏈接到 runtime.reflectcall 這個(gè)用匯編實(shí)現(xiàn)的函數(shù)上,我們?cè)谶@里并不會(huì)展開介紹該函數(shù)的具體實(shí)現(xiàn),感興趣的讀者可以自行了解其實(shí)現(xiàn)原理。
處理返回值
當(dāng)函數(shù)調(diào)用結(jié)束之后,我們就會(huì)開始處理函數(shù)的返回值了,如果函數(shù)沒有任何返回值我們就會(huì)直接清空 args 中的全部?jī)?nèi)容來釋放內(nèi)存空間,不過如果當(dāng)前函數(shù)有返回值就會(huì)進(jìn)入另一個(gè)分支:
復(fù)制代碼
var ret []Value if nout == 0 { typedmemclr(frametype, args) framePool.Put(args) } else { typedmemclrpartial(frametype, args, 0, retOffset) ret = make([]Value, nout) off = retOffset for i := 0; i < nout; i++ { tv := t.Out(i) a := uintptr(tv.Align()) off = (off + a - 1) &^ (a - 1) if tv.Size() != 0 { fl := flagIndir | flag(tv.Kind()) ret[i] = Value{tv.common(), add(args, off, "tv.Size() != 0"), fl} } else { ret[i] = Zero(tv) } off += tv.Size() } } return ret}
- 將 args 中與輸入?yún)?shù)有關(guān)的內(nèi)存空間清空;
- 創(chuàng)建一個(gè) nout 長(zhǎng)度的切片用于保存由反射對(duì)象構(gòu)成的返回值數(shù)組;
- 從函數(shù)對(duì)象中獲取返回值的類型和內(nèi)存大小,將 args 內(nèi)存中的數(shù)據(jù)轉(zhuǎn)換成 reflect.Value 類型的返回值;
由 reflect.Value 構(gòu)成的 ret 數(shù)組最終就會(huì)被返回到上層,使用反射進(jìn)行函數(shù)調(diào)用的過程也就結(jié)束了。
總結(jié)
我們?cè)谶@一節(jié)中 Go 語言的 reflect 包為我們提供的多種能力,其中包括如何使用反射來動(dòng)態(tài)修改變量、判斷類型是否實(shí)現(xiàn)了某些協(xié)議以及動(dòng)態(tài)調(diào)用方法,通過對(duì)反射包中方法原理的分析幫助我們理解之前看起來比較怪異、令人困惑的現(xiàn)象。