调用惯例

对于一个简单的函数:

  1. package main
  2. func myFunction(a, b int) (int, int) {
  3. return a + b, a - b
  4. }
  5. func main() {
  6. myFunction(66, 77)
  7. }

myFunction函数接收两个整数并返回两个整数。

通过go tool compile 获取汇编指令
go tool compile -S -N -l main.go

  1. "".main STEXT size=68 args=0x0 locals=0x28
  2. 0x0000 00000 (main.go:7) MOVQ (TLS), CX
  3. 0x0009 00009 (main.go:7) CMPQ SP, 16(CX)
  4. 0x000d 00013 (main.go:7) JLS 61
  5. 0x000f 00015 (main.go:7) SUBQ $40, SP // 分配 40 字节栈空间
  6. 0x0013 00019 (main.go:7) MOVQ BP, 32(SP) // 将基址指针存储到栈上
  7. 0x0018 00024 (main.go:7) LEAQ 32(SP), BP
  8. 0x001d 00029 (main.go:8) MOVQ $66, (SP) // 第一个参数
  9. 0x0025 00037 (main.go:8) MOVQ $77, 8(SP) // 第二个参数
  10. 0x002e 00046 (main.go:8) CALL "".myFunction(SB)
  11. 0x0033 00051 (main.go:9) MOVQ 32(SP), BP
  12. 0x0038 00056 (main.go:9) ADDQ $40, SP
  13. 0x003c 00060 (main.go:9) RET

根据main函数生成的汇编指令,可以分析出main函数调用myFunction之前的栈
image.png
main 函数通过 SUBQ $40, SP 指令一共在栈上分配了 40 字节的内存空间:

空间 大小 作用
SP+32 ~ BP 8 字节 main 函数的栈基址指针
SP+16 ~ SP+32 16 字节 函数 myFunction 的两个返回值
SP ~ SP+16 16 字节 函数 myFunction 的两个参数

表 4-1 主函数调用栈

myFunction 入参的压栈顺序和 C 语言一样,都是从右到左,即第一个参数 66 在栈顶的 SP ~ SP+8,第二个参数存储在 SP+8 ~ SP+16 的空间中。

小结:
函数的入参和返回值,从右到左被压入栈中,即最左边的入参在栈顶,最右边的返回值在栈底(base pointer 之上)

随后调用myFunction :CALL "".myFunction(SB)
这个指令会将main函数的返回地址压入栈顶,随后调用fyFunction汇编指令

  1. "".myFunction STEXT nosplit size=49 args=0x20 locals=0x0
  2. 0x0000 00000 (main.go:3) MOVQ $0, "".~r2+24(SP) // 初始化第一个返回值
  3. 0x0009 00009 (main.go:3) MOVQ $0, "".~r3+32(SP) // 初始化第二个返回值
  4. 0x0012 00018 (main.go:4) MOVQ "".a+8(SP), AX // AX = 66
  5. 0x0017 00023 (main.go:4) ADDQ "".b+16(SP), AX // AX = AX + 77 = 143
  6. 0x001c 00028 (main.go:4) MOVQ AX, "".~r2+24(SP) // (24)SP = AX = 143
  7. 0x0021 00033 (main.go:4) MOVQ "".a+8(SP), AX // AX = 66
  8. 0x0026 00038 (main.go:4) SUBQ "".b+16(SP), AX // AX = AX - 77 = -11
  9. 0x002b 00043 (main.go:4) MOVQ AX, "".~r3+32(SP) // (32)SP = AX = -11
  10. 0x0030 00048 (main.go:4) RET

myFunction首先对返回值进行初始化,再进行计算并存入栈中

随后main函数恢复栈基指针并销毁失去作用的40字节栈内存

小结:
Go语言使用栈 传递参数 和 接收返回值 ,所以只需要在栈上多分配一些内存就可以返回多个值

参数传递

值传递和引用传递:

  • 传值:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据;
  • 传引用:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方

Go语言选用了传值的方式,无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝

将指针作为参数传入某个函数时,函数内部会复制指针,也就是会同时出现两个指针指向原有的内存空间,所以 Go 语言中传指针也是传值。