深入分析golang多值返回以及闭包的实现

2020-01-28 12:05:40王旭

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字节,这有待考证。

本来想通过查看main函数的栈帧来验证之前的结论,但是golang对小函数自动转为内联函数,因此你们可以自己编译出来看看,main函数内部是没有调用test函数的,而是将test函数的汇编代码直接拷贝进main函数执行了。

四、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 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