深刻分析golang多值返回以及閉包的實現

1、前言golang

golang有不少新穎的特性,不知道你們的使用的時候,有沒想過,這些特性是如何實現的?固然你可能會說,不瞭解這些特性好像也不影響本身使用golang,你說的也有道理,可是,多瞭解底層的實現原理,對於在使用golang時的眼界是徹底不同的,就相似於看過http的實現以後,再來使用http框架,和未看過http框架時的眼界是不同的,固然,你若是是一名it愛好者,求知慾天然會引導你去學習。小程序

2、這篇文章主要就分析兩點:閉包

     一、golang多值返回的實現;框架

     二、golang閉包的實現;函數

3、golang多值返回的實現學習

咱們在學C/C++時,不少人應該有了解過C/C++函數調用過程,參數是經過寄存器di和si(假設就兩個參數)傳遞給被調用的函數,被調用函數的返回結果只能是經過eax寄存器返回給調用函數,所以C/C++函數只能返回一個值,那麼咱們是否是能夠想象,golang的多值返回是否能夠經過多個寄存器來實現的,正如用多個寄存器來傳參同樣?測試

這也是一種辦法,可是golang並無採用;個人理解是引入多個寄存器來存儲返回值,會引發多個寄存器用途的從新約定,這無疑增長了複雜度;能夠這麼說,golang的ABI與C/C++很是不同;ui

在從彙編角度分析golang多值返回以前,須要先熟悉golang彙編代碼的一些約定, golang官網 有說明,這裏重點說明四個symbols,須要注意的是這裏的寄存器是僞寄存器:this

       1.FP 棧底寄存器,指向一個函數棧的頂部;spa

       2.PC 程序計數器,指向下一條執行指令;

       3.SB 指向靜態數據的基指針,全局符號;

       4.SP 棧頂寄存器;

這裏面最重要的就是FP和SP,FP寄存器主要用於取參數以及存返回值,golang函數調用的實現很大程度上都是依賴這兩個寄存器,這裏先給出結果,

+-----------+---\
| 返回值2 | \
+-----------+  \
| 返回值1 |  \
+---------+-+  
| 參數2 |  這些在調用函數中
+-----------+  
| 參數1 |   /
+-----------+  /
| 返回地址 | /
+-----------+--\/-----fp值
| 局部變量 | \
| ... | 被調用數棧禎
|   | /
+-----------+--/+---sp值


這個就是golang的一個函數棧,也是說函數傳參是經過fp+offset來實現的,而多個返回值也是經過fp+offset存儲在調用函數的棧幀中。

下面經過一個例子來分析

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函數的彙編代碼

"".test t=1size=32value=0args=0x20locals=0x0
0x000000000(test.go:5) TEXT"".test(SB),$0-32//棧大小爲32字節
0x000000000(test.go:5)NOP
0x000000000(test.go:5)NOP
0x000000000(test.go:5)MOVQ"".i+8(FP),CX//取第一個參數i
0x000500005(test.go:5)MOVQ"".j+16(FP),AX//取第二個參數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
 //將返回結果存入調用函數棧幀
0x001300019(test.go:8)MOVQCX,"".~r2+24(FP)
 //將返回結果存入調用函數棧幀
0x001800024(test.go:8)MOVQBX,"".~r3+32(FP)
0x001d00029(test.go:8)RET

由這個彙編代碼能夠看出來,在test函數內部,是經過fp+8取第一個參數,fp+16取第二個參數;而後將返回的第一個值存入fp+24,返回的第二個值存入fp+32,和我上述所說徹底一致;golang函數調用過程,是經過fp+offset來實現傳參和返回值,而不像C/C++都是經過寄存器實現傳參和返回值;
可是,這裏有個問題,個人變量都是int類型,爲啥分配的都是8字節,這有待考證。由這個彙編代碼能夠看出來,在test函數內部,是經過fp+8取第一個參數,fp+16取第二個參數;而後將返回的第一個值存入fp+24,返回的第二個值存入fp+32,和我上述所說徹底一致;golang函數調用過程,是經過fp+offset來實現傳參和返回值,而不像C/C++都是經過寄存器實現傳參和返回值;

原本想經過查看main函數的棧幀來驗證以前的結論,可是golang對小函數自動轉爲內聯函數,所以大家能夠本身編譯出來看看,main函數內部是沒有調用test函數的,而是將test函數的彙編代碼直接拷貝進main函數執行了。

4、golang閉包的實現

以前有去看了下C++11的lambda函數的實現,其實實現原理就是仿函數;編譯器在編譯lambda函數時,會生成一個匿名的仿函數類,而後執行這個lambda函數時,會調用編譯生成的匿名仿函數類重載函數調用方法,這個方法也就是lambda函數中定義的方法;其實golang閉包的實現和這個相似,咱們經過例子來講明

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函數傳入一個整型參數a,返回一個函數類型;這個函數類型傳入一個整型參數以及返回一個整型值;main函數調用test函數,返回一個閉包函數。
來看下test函數的彙編代碼:這個例子程序很簡單,test函數傳入一個整型參數a,返回一個函數類型;這個函數類型傳入一個整型參數以及返回一個整型值;main函數調用test函數,返回一個閉包函數。

"".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函數傳入的第一個參數,即a
0x002d00045(test.go:5)MOVQ"".a+40(FP),BP
 //將a放入(AX)指向的內存,即上述新生成的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內部匿名函數地址存入BP
0x004a00074(test.go:6) LEAQ"".test.func1(SB),BP
 //將匿名函數地址放入(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指向內存+8,
 //即爲上述結構體a *int賦值
0x006700103(test.go:6)MOVQBP,8(AX)
 //將上述結構體的地址存入main函數棧幀中;
0x006b00107(test.go:9)MOVQAX,"".~r1+48(FP)
0x007000112(test.go:9) ADDQ$32,SP
0x007400116(test.go:9)RET

以前有看到一句話,很形象地描述了閉包

類是有行爲的數據,爲閉包是有數據的行爲;

也就是說閉包是有上下文的,咱們以測試例子爲例,經過test函數生成的閉包函數,都有各自的a,這個a就是閉包的上下文數據,並且這個a一直伴隨着他的閉包函數,每調用一次,a都會發生變化;

咱們分析了上述彙編代碼,來看下閉包實現原理;在這個測試例子中,因爲a是閉包的上下文數據,所以a必須在堆上分配,若是在棧上分配,函數結束,a也被回收了;而後會定義出一個匿名結構體:

type.struct{
 F uintptr//這個就是閉包調用的函數指針
 a *int//這就是閉包的上下文數據
}

接着生成一個該對象,並將以前在堆上分配的整型對象a的地址賦值給結構體中的a指針,接下來將閉包調用的func函數地址賦值給結構體中F指針;這樣,每生成一個閉包函數,其實就是生成一個上述結構體對象,每一個閉包對象也就有本身的數據a和調用函數F;最後將這個結構體的地址返回給main函數;

來看下main函數獲取閉包的過程;

"".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)
 //將參數1(f:=test(1))放入main函數棧頂
0x001f00031(test.go:13)MOVQ$1, (SP)
0x002700039(test.go:13) PCDATA$0,$0
 //調用main函數生成閉包對象
0x002700039(test.go:13)CALL"".test(SB)
 //將閉包對象的地址放入DX
0x002c00044(test.go:13)MOVQ8(SP),DX
 //將參數2(a:=f(2))放入棧頂
0x003100049(test.go:14)MOVQ$2, (SP)
0x003900057(test.go:14)MOVQDX,"".f+56(SP)
 //將閉包對象的函數指針賦值給BX
0x003e00062(test.go:14)MOVQ(DX),BX
0x004100065(test.go:14) PCDATA$0,$1
 //這裏調用閉包函數,而且將閉包對象的地址也傳進
 //閉包函數,爲了修改a嘛
0x004100065(test.go:14)CALLDX,BX
0x004300067(test.go:14)MOVQ8(SP),BX

 

很明顯,main函數調用test函數獲取的是閉包對象的地址,經過這個閉包對象地址找到閉包函數,而後執行這個閉包函數,而且把閉包對象的地址傳進函數,這點和C++傳this指針原理同樣,爲了修改爲員變量a

最後看下test內部的匿名函數(閉包函數實現):

"".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
//將參數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地址最新數據存入BP
0x001200018(test.go:8) MOVQ (AX), BP
//將a最新值做爲返回值放入main函數棧中
0x001500021(test.go:8) MOVQ BP,"".~r1+16(FP)
0x001a00026(test.go:8) RET

閉包函數的調用過程:

      一、經過閉包對象地址獲取閉包上下文數據a的地址;

      二、接着經過a的地址獲取到a的值,並與參數i相加;

      三、將a+i做爲最新值存入a的地址;

      四、將a最新值返回給main函數;

5、總結

這篇文章簡單地從彙編角度分析了golang多值返回和閉包的實現;

      多值返回主要是經過fp寄存器+offset獲取參數以及存入返回值實現;

      閉包主要是經過在編譯時生成包含閉包函數和閉包上下文數據的結構體實現;

相關文章
相關標籤/搜索