您好,登錄后才能下訂單哦!
一、前言
golang有很多新穎的特性,不知道大家的使用的時候,有沒想過,這些特性是如何實現(xiàn)的?當然你可能會說,不了解這些特性好像也不影響自己使用golang,你說的也有道理,但是,多了解底層的實現(xiàn)原理,對于在使用golang時的眼界是完全不一樣的,就類似于看過http的實現(xiàn)之后,再來使用http框架,和未看過http框架時的眼界是不一樣的,當然,你如果是一名it愛好者,求知欲自然會引導(dǎo)你去學(xué)習(xí)。
二、這篇文章主要就分析兩點:
1、golang多值返回的實現(xiàn);
2、golang閉包的實現(xiàn);
三、golang多值返回的實現(xiàn)
我們在學(xué)C/C++時,很多人應(yīng)該有了解過C/C++函數(shù)調(diào)用過程,參數(shù)是通過寄存器di和si(假設(shè)就兩個參數(shù))傳遞給被調(diào)用的函數(shù),被調(diào)用函數(shù)的返回結(jié)果只能是通過eax寄存器返回給調(diào)用函數(shù),因此C/C++函數(shù)只能返回一個值,那么我們是不是可以想象,golang的多值返回是否可以通過多個寄存器來實現(xiàn)的,正如用多個寄存器來傳參一樣?
這也是一種辦法,但是golang并沒有采用;我的理解是引入多個寄存器來存儲返回值,會引起多個寄存器用途的重新約定,這無疑增加了復(fù)雜度;可以這么說,golang的ABI與C/C++非常不一樣;
在從匯編角度分析golang多值返回之前,需要先熟悉golang匯編代碼的一些約定, golang官網(wǎng) 有說明,這里重點說明四個symbols,需要注意的是這里的寄存器是偽寄存器:
1.FP 棧底寄存器,指向一個函數(shù)棧的頂部;
2.PC 程序計數(shù)器,指向下一條執(zhí)行指令;
3.SB 指向靜態(tài)數(shù)據(jù)的基指針,全局符號;
4.SP 棧頂寄存器;
這里面最重要的就是FP和SP,F(xiàn)P寄存器主要用于取參數(shù)以及存返回值,golang函數(shù)調(diào)用的實現(xiàn)很大程度上都是依賴這兩個寄存器,這里先給出結(jié)果,
+-----------+---\ | 返回值2 | \ +-----------+ \ | 返回值1 | \ +---------+-+ | 參數(shù)2 | 這些在調(diào)用函數(shù)中 +-----------+ | 參數(shù)1 | / +-----------+ / | 返回地址 | / +-----------+--\/-----fp值 | 局部變量 | \ | ... | 被調(diào)用數(shù)棧禎 | | / +-----------+--/+---sp值
這個就是golang的一個函數(shù)棧,也是說函數(shù)傳參是通過fp+offset
來實現(xiàn)的,而多個返回值也是通過fp+offset
存儲在調(diào)用函數(shù)的棧幀中。
下面通過一個例子來分析
package main import "fmt" func test(i, j int) (int, int) { a:=i+ j b:=i- j return a,b } func main() { a,b:= test(2,1) fmt.Println(a, b) }
這個例子很簡單,主要是為了說明golang多值返回的過程;我們通過下面命令編譯該程序
go tool compile -S test.go > test.s
然后,就可以打開test.s,來看下這個小程序的匯編代碼。首先來看下test函數(shù)的匯編代碼
"".test t=1size=32value=0args=0x20locals=0x0 0x000000000(test.go:5) TEXT"".test(SB),$0-32//棧大小為32字節(jié) 0x000000000(test.go:5)NOP 0x000000000(test.go:5)NOP 0x000000000(test.go:5)MOVQ"".i+8(FP),CX//取第一個參數(shù)i 0x000500005(test.go:5)MOVQ"".j+16(FP),AX//取第二個參數(shù)j 0x000a00010(test.go:5) FUNCDATA$0, gclocals·a8eabfc4a4514ed6b3b0c61e9680e440(SB) 0x000a00010(test.go:5) FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x000a00010(test.go:6)MOVQCX,BX//將i放入bx 0x000d00013(test.go:6) ADDQAX,CX//i+j放入cx 0x001000016(test.go:7) SUBQAX,BX//i-j放入bx //將返回結(jié)果存入調(diào)用函數(shù)棧幀 0x001300019(test.go:8)MOVQCX,"".~r2+24(FP) //將返回結(jié)果存入調(diào)用函數(shù)棧幀 0x001800024(test.go:8)MOVQBX,"".~r3+32(FP) 0x001d00029(test.go:8)RET
由這個匯編代碼可以看出來,在test
函數(shù)內(nèi)部,是通過fp+8取第一個參數(shù),fp+16
取第二個參數(shù);然后將返回的第一個值存入fp+24
,返回的第二個值存入fp+32
,和我上述所說完全一致;golang函數(shù)調(diào)用過程,是通過fp+offset
來實現(xiàn)傳參和返回值,而不像C/C++都是通過寄存器實現(xiàn)傳參和返回值;
但是,這里有個問題,我的變量都是int類型,為啥分配的都是8字節(jié),這有待考證。
本來想通過查看main函數(shù)的棧幀來驗證之前的結(jié)論,但是golang對小函數(shù)自動轉(zhuǎn)為內(nèi)聯(lián)函數(shù),因此你們可以自己編譯出來看看,main函數(shù)內(nèi)部是沒有調(diào)用test函數(shù)的,而是將test函數(shù)的匯編代碼直接拷貝進main函數(shù)執(zhí)行了。
四、golang閉包的實現(xiàn)
之前有去看了下C++11的lambda
函數(shù)的實現(xiàn),其實實現(xiàn)原理就是仿函數(shù);編譯器在編譯lambda
函數(shù)時,會生成一個匿名的仿函數(shù)類,然后執(zhí)行這個lambda
函數(shù)時,會調(diào)用編譯生成的匿名仿函數(shù)類重載函數(shù)調(diào)用方法,這個方法也就是lambda
函數(shù)中定義的方法;其實golang閉包的實現(xiàn)和這個類似,我們通過例子來說明
packagemain import"fmt" functest(aint)func(iint)int{ returnfunc(iint)int{ a = a + i returna } } funcmain(){ f := test(1) a := f(2) fmt.Println(a) b := f(3) fmt.Println(b) }
這個例子程序很簡單,test
函數(shù)傳入一個整型參數(shù)a
,返回一個函數(shù)類型;這個函數(shù)類型傳入一個整型參數(shù)以及返回一個整型值;main
函數(shù)調(diào)用test
函數(shù),返回一個閉包函數(shù)。
來看下test
函數(shù)的匯編代碼:
"".test t=1size=160value=0args=0x10locals=0x20 0x000000000(test.go:5) TEXT"".test(SB),$32-16 0x000000000(test.go:5)MOVQ(TLS),CX 0x000900009(test.go:5) CMPQSP,16(CX) 0x000d00013(test.go:5) JLS142 0x000f00015(test.go:5) SUBQ$32,SP 0x001300019(test.go:5) FUNCDATA$0, gclocals·8edb5632446ada37b0a930d010725cc5(SB) 0x001300019(test.go:5) FUNCDATA$1, gclocals·008e235a1392cc90d1ed9ad2f7e76d87(SB) 0x001300019(test.go:5) LEAQ type.int(SB),BX 0x001a00026(test.go:5)MOVQBX, (SP) 0x001e00030(test.go:5) PCDATA$0,$0 //生成一個int型對象,即a 0x001e00030(test.go:5)CALLruntime.newobject(SB) //8(sp)即生成的a的地址,放入AX 0x002300035(test.go:5)MOVQ8(SP),AX //將a的地址存入sp+24的位置 0x002800040(test.go:5)MOVQAX,"".&a+24(SP) //取出main函數(shù)傳入的第一個參數(shù),即a 0x002d00045(test.go:5)MOVQ"".a+40(FP),BP //將a放入(AX)指向的內(nèi)存,即上述新生成的int型對象 0x003200050(test.go:5)MOVQBP, (AX) 0x003500053(test.go:6) LEAQ type.struct { F uintptr; a *int }(SB), BX 0x003c00060(test.go:6)MOVQBX, (SP) 0x004000064(test.go:6) PCDATA$0,$1 0x004000064(test.go:6)CALLruntime.newobject(SB) //8(sp)這就是上述生成的struct對象地址 0x004500069(test.go:6)MOVQ8(SP),AX 0x004a00074(test.go:6)NOP //test內(nèi)部匿名函數(shù)地址存入BP 0x004a00074(test.go:6) LEAQ"".test.func1(SB),BP //將匿名函數(shù)地址放入(AX)指向的地址,即給上述 //F uintptr賦值 0x005100081(test.go:6)MOVQBP, (AX) 0x005400084(test.go:6)MOVQAX,"".autotmp_0001+16(SP) 0x005900089(test.go:6)NOP //將上述生成的整型對象a的地址存入BP 0x005900089(test.go:6)MOVQ"".&a+24(SP),BP 0x005e00094(test.go:6) CMPB runtime.writeBarrier(SB),$0 0x006500101(test.go:6)JNE$0,117 //將a地址存入AX指向內(nèi)存+8, //即為上述結(jié)構(gòu)體a *int賦值 0x006700103(test.go:6)MOVQBP,8(AX) //將上述結(jié)構(gòu)體的地址存入main函數(shù)棧幀中; 0x006b00107(test.go:9)MOVQAX,"".~r1+48(FP) 0x007000112(test.go:9) ADDQ$32,SP 0x007400116(test.go:9)RET
之前有看到一句話,很形象地描述了閉包
類是有行為的數(shù)據(jù),為閉包是有數(shù)據(jù)的行為;
也就是說閉包是有上下文的,我們以測試例子為例,通過test
函數(shù)生成的閉包函數(shù),都有各自的a,這個a
就是閉包的上下文數(shù)據(jù),而且這個a
一直伴隨著他的閉包函數(shù),每調(diào)用一次,a
都會發(fā)生變化;
我們分析了上述匯編代碼,來看下閉包實現(xiàn)原理;在這個測試例子中,由于a
是閉包的上下文數(shù)據(jù),因此a
必須在堆上分配,如果在棧上分配,函數(shù)結(jié)束,a
也被回收了;然后會定義出一個匿名結(jié)構(gòu)體:
type.struct{ F uintptr//這個就是閉包調(diào)用的函數(shù)指針 a *int//這就是閉包的上下文數(shù)據(jù) }
接著生成一個該對象,并將之前在堆上分配的整型對象a
的地址賦值給結(jié)構(gòu)體中的a指針,接下來將閉包調(diào)用的func
函數(shù)地址賦值給結(jié)構(gòu)體中F
指針;這樣,每生成一個閉包函數(shù),其實就是生成一個上述結(jié)構(gòu)體對象,每個閉包對象也就有自己的數(shù)據(jù)a
和調(diào)用函數(shù)F
;最后將這個結(jié)構(gòu)體的地址返回給main
函數(shù);
來看下main
函數(shù)獲取閉包的過程;
"".main t=1size=528value=0args=0x0locals=0x88 0x000000000(test.go:12) TEXT"".main(SB),$136-0 0x000000000(test.go:12)MOVQ(TLS),CX 0x000900009(test.go:12) LEAQ -8(SP),AX 0x000e00014(test.go:12) CMPQAX,16(CX) 0x001200018(test.go:12) JLS506 0x001800024(test.go:12) SUBQ$136,SP 0x001f00031(test.go:12) FUNCDATA$0, gclocals·f5be5308b59e045b7c5b33ee8908cfb7(SB) 0x001f00031(test.go:12) FUNCDATA$1, gclocals·9d868b227cedd8dd4b1bec8682560fff(SB) //將參數(shù)1(f:=test(1))放入main函數(shù)棧頂 0x001f00031(test.go:13)MOVQ$1, (SP) 0x002700039(test.go:13) PCDATA$0,$0 //調(diào)用main函數(shù)生成閉包對象 0x002700039(test.go:13)CALL"".test(SB) //將閉包對象的地址放入DX 0x002c00044(test.go:13)MOVQ8(SP),DX //將參數(shù)2(a:=f(2))放入棧頂 0x003100049(test.go:14)MOVQ$2, (SP) 0x003900057(test.go:14)MOVQDX,"".f+56(SP) //將閉包對象的函數(shù)指針賦值給BX 0x003e00062(test.go:14)MOVQ(DX),BX 0x004100065(test.go:14) PCDATA$0,$1 //這里調(diào)用閉包函數(shù),并且將閉包對象的地址也傳進 //閉包函數(shù),為了修改a嘛 0x004100065(test.go:14)CALLDX,BX 0x004300067(test.go:14)MOVQ8(SP),BX
很明顯,main
函數(shù)調(diào)用test
函數(shù)獲取的是閉包對象的地址,通過這個閉包對象地址找到閉包函數(shù),然后執(zhí)行這個閉包函數(shù),并且把閉包對象的地址傳進函數(shù),這點和C++傳this指針原理一樣,為了修改成員變量a
;
最后看下test
內(nèi)部的匿名函數(shù)(閉包函數(shù)實現(xiàn)):
"".test.func1t=1size=32value=0args=0x10 locals=0x0 0x000000000(test.go:6) TEXT"".test.func1(SB), $0-16 0x000000000(test.go:6) NOP 0x000000000(test.go:6) NOP 0x000000000(test.go:6) FUNCDATA $0, gclocals·23e8278e2b69a3a75fa59b23c49ed6ad(SB) 0x000000000(test.go:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) //DX是閉包對象的地址,+8即a的地址 0x000000000(test.go:6) MOVQ8(DX), AX //AX為a的地址,(AX)即為a的值 0x000400004(test.go:7) MOVQ (AX), BP //將參數(shù)i存入R8 0x000700007(test.go:7) MOVQ"".i+8(FP), R8 //a+i的值存入BP 0x000c00012(test.go:7) ADDQ R8, BP //將a+i存入a的地址 0x000f00015(test.go:7) MOVQ BP, (AX) //將a地址最新數(shù)據(jù)存入BP 0x001200018(test.go:8) MOVQ (AX), BP //將a最新值作為返回值放入main函數(shù)棧中 0x001500021(test.go:8) MOVQ BP,"".~r1+16(FP) 0x001a00026(test.go:8) RET
閉包函數(shù)的調(diào)用過程:
1、通過閉包對象地址獲取閉包上下文數(shù)據(jù)a的地址;
2、接著通過a的地址獲取到a的值,并與參數(shù)i相加;
3、將a+i作為最新值存入a的地址;
4、將a最新值返回給main函數(shù);
五、總結(jié)
這篇文章簡單地從匯編角度分析了golang多值返回和閉包的實現(xiàn);
多值返回主要是通過fp寄存器+offset獲取參數(shù)以及存入返回值實現(xiàn);
閉包主要是通過在編譯時生成包含閉包函數(shù)和閉包上下文數(shù)據(jù)的結(jié)構(gòu)體實現(xiàn);
以上就是這篇文章的全部內(nèi)容,希望對大家學(xué)習(xí)或只用golang能有一定的幫助,如果有疑問大家可以留言交流。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。