一个例子
我们知道,由于Go语言是值传递,如果在被调用函数内部交换两个数的值,调用者内部对应的实参是不会发生变化的,就如下所示:
但这个现象的原因从函数调用栈来说具体的机理是什么呢?我们通过函数调用栈看看问题到底出在哪
由于函数调用没有返回值,所以局部变量后面就是给被调用函数传入的参数$args$,注意调用函数的参数入栈顺序由右到左,返回值也是一样(原因是这样被调用函数通过**$sp$+偏移寻址**就比较方便了)
可以看出,当swap
函数交换两数时,交换的是args
内的$a、b$,而不是main
函数中的局部变量,所以main
函数中的$a、b$交换失败。
传指针的例子
同理与上面的例子,我们便可以很容易理解传指针时为什么可以成功交换$a、b$的值
此时的栈帧空间分布如下:
swap
执行到*a,*b=*b,*a
时,交换的是这两个指针指向的数据,也就是这两个地址的数据,所以这一次能交换成功
匿名函数返回值
Go语言支持多返回值,所以在栈上分配返回值空间更合适
首先,栈帧上所有的$a、b$初始均等于0,执行到①处时,被调用函数的参数args
上的$a$自增为1,接着执行到②,incr
函数栈帧的$b$被赋值等于1。
注:defer与return时机
return赋值和返回是两个步骤,不是原子操作,如果有defer会插在两个步骤中:
- 返回值赋值(return value)
- defer语句
- 返回值真正返回,调用函数结束
所以return
函数先将incr
函数栈帧的$b$赋值给返回值,也就是此时的栈空间是这样的
接着执行defer
函数,args
上的$a$再自增1,incr
函数局部变量$b$也自增1,然后incr
结束。此时返回值为1,所以main
函数中的$b$最终被赋值为1,而main
函数中的$a$并不会收到incr
函数的影响,值仍然为0。最终的栈内布局是这样的:
所以最终输出的是0和1
具名返回值函数
假如我们其他都不变,只把这里的局部变量b,改成命名返回值,看看有什么不同
当执行到a++
时,参数$a$自增1,由于是具名返回值函数,此时返回值$b$被赋予为1
接着defer
函数,参数$a$再自增1,返回值b也自增1,incr
结束,所以最终返回值$b$的值为2
调用多个函数时的小问题
如果一个函数A调用了两个函数B和C。但是这两个函数的参数和返回值占用的空间并不相同,而Go语言的函数栈帧是一次性分配的,所以要以最大的参数加返回值空间为标准来分配栈帧空间,才能满足所有被调函数的需求
假设B的参数和返回值占用的空间大,当调用B时,B的参数和返回值可以把分配的参数加返回值空间占满没有问题,但是调用B时,B的参数和返回值只会占用靠近栈顶的那部分空间(即上图方框中靠下的部分)
原因是虽然上面空出来一块,但是被调用者通过栈指针相对寻址自己的参数和返回值时会比较方便
参考
幼麟实验室的Golang合辑