Golang底层原理剖析之函数调用栈-传参和返回值
defer与return时机
return赋值和返回是两个步骤,不是原子操作,如果有defer会插在两个步骤中:
- 返回值赋值(return value)
- defer语句 //可有可无
- 返回值返回
传值的swap函数
我们通过函数调用栈看看问题到底出在哪
假设main函数栈帧在这里,先分配局部变量locals,这里函数调用没有返回值,所以局部后面就是给被调用函数传入的参数args,注意参数入栈顺序由右到左,返回值也是一样,这样被调用函数通过sp+偏移寻址就比较方便了,调用者栈帧后面存的是下一条指令的地址,在下面分配的就是swap函数栈帧了,当swap函数执行到a,b=b,a时,要交换两个参数的值
把值交换一下
现在,交换失败的原因找到了,调用者的局部变量a和b在这里,交换的并不是它们
传指针的swap函数
我们通过函数调用栈看看和上一次有什么不同
main函数栈帧先分配局部变量,然后分配参数空间,参数是指针,传参都是值拷贝,这里拷贝的就是a和b的地址,在后面是返回地址以及swap函数栈帧
swap执行到*a,*b=*b,*a时,交换的是这两个指针指向的数据,也就是这两个地址的数据,所以这一次能交换成功
匿名返回值函数
通常我们认为返回值是通过寄存器传递的,但是go语言支持多返回值,所以在栈上分配返回值空间更合适
这里main函数调用incr函数,然后赋给局部变量b,来看看函数调用栈的情况。
main函数栈帧,先是局部变量a=0,b=0,然后是incr的返回值,初始化为类型零值,然后是参数,传参值拷贝,最后是返回地址。到incr函数栈帧这里,保存调用者main的栈帧地址后,初始化局部变量b
执行到这里,要把参数a自增1,而参数a在这
下一步,把参数a赋给局部变量b。到return这里,必须要明确一个关键问题。我们说过函数最后有编译器插入的指令,负责释放函数栈帧,恢复到调用者栈,但在这之前要给返回值赋值并执行defer函数,那谁先?谁后?答案是先赋值
所以执行到return b这里,会先把局部变量b的值拷贝到返回值空间
然后再执行注册的defer函数,defer函数里,这一步a再次自增1,下一步局部变量b也自增1,然后incr结束。
返回值为1 ,赋给main函数的局部变量b,所以最后会输出0和1
具名返回值函数
其他都不变,只把这里的局部变量b,改成命名返回值,看看有什么不同
main函数栈帧与上一例完全相同,到incr函数栈帧这里,没有局部变量,当执行到a++时,参数a自增1
return这里,先把参数a赋给返回值b
然后执行defer函数,参数a再次自增1,下一步,返回值b也自增1,然后incr结束,返回值最终为2,所以main的局部变量b赋值为2,最终输出0和2
调用多个函数的小问题
如果一个函数A调用了两个函数B和C。但是这两个函数的参数和返回值,占用的空间并不相同,我们知道Go语言的函数栈帧是一次性分配的,如果局部变量占这么大,这后面还要以最大的参数加返回值空间为标准来分配,才能满足所有被调函数的需求
B的参数和返回值可以把这里占满没有问题
但是B结束后调用C时,它的参数和返回值,只会占用下面这段空间,虽然上面空出来一块,但是被调用者,通过栈指针相对寻址自己的参数和返回值时会比较方便