Alomerry Wu @ alomerry.com

深入理解 Golang 函数调用

Jul 5, 2023 · 8min · 2k ·

函数栈

了解 go 的函数调用底层逻辑,能更清晰的理解 defer、recover、panic 的工作方式。go 的函数栈如下:

golang-function-stack-frame

栈从高地址往低地址依次是:

  • 栈基
  • 局部标量
  • 调用函数返回值
  • 调用函数参数
  • 函数返回地址
  • 栈指针
func main() {
  int a = 5;
  println(a);
}

在为 main 函数分配函数栈时,会将使用两个 int 大小的空间 r1、r2 分别存储变量 a 和传入 println 函数的参数 a。你可能会好奇为什么 a 需要占两份,因为对于 go 来说,函数参数皆是值拷贝,只是区别是拷贝目标对象还是拷贝指针,函数调用前就会一次性为形参和返回值分配内存空间,并将实参拷贝到形参中。由于 println 没有返回值,所以仅有两个变量 a 的空间。

指令

理解了以上后,就需要知道一些额外的指令

call 指令

  • 将下一条指令入栈,作为返回地址
  • 跳转到被调用者函数执行

执行函数前的处理:

  • 入栈调用函数栈的栈基
  • 分配函数栈
  • 设置被调用函数的栈基(bp)

执行函数 给返回值赋值 执行 defer 函数

执行 ret 指令前的处理:

  • 恢复调用函数的栈基
  • 释放被调函数的函数栈

ret 指令

  • 弹出调用前入栈的返回地址
  • 跳转到该返回地址

Base Stack

传值/传指针

传值
func swap(a, b int) {
  a, b = b, a
}

func main() {
  a, b = 1, 2
  swap(a, b)
  fmt.Println(a, b) // 1, 2
}

分析以上代码,main 方法在执行时会给局部变量 ab,调用 swap 的两个参数(参数从右往左)分配栈空间并赋值如下:

栈帧
局部变量 a = 1main BP
局部变量 b = 2
swap 参数 b = 2
swap 参数 a = 1
返回地址main SP
main BP

swap 方法中会将参数 ab 交换:

栈帧
局部变量 a = 1main BP
局部变量 b = 2
swap 参数 b = 1
swap 参数 a = 2
返回地址main SP
main BP

可以看到,swap 方法并没有修改调用者的变量,因此 main 方法中的 ab 交换失败了。

传指针
func swap(a, b *int) {
  a, b = b, a
}

func main() {
  a, b = 1, 2
  swap(&a, &b)
  fmt.Println(a, b) // 1, 2
}
栈帧
addrAa = 1main BP
addrBb = 2
swap 参数 b = addrB
swap 参数 a = addrA
返回地址main SP
main BP

可以看到,此时 swap 方法交换的是 addrAaddrB 对应的数据,此时就可以交换成功了。

Function Receiver

type A struct {
}

func (A) F1(string) string {
  return ""
}

func (*A) F2(string) string {
  return ""
}

func f1(A, string) string {
  return "xxx"
}

func f2(*A, string) string {
  return "xxx"
}

func main() {
  fmt.Println(reflect.TypeOf(A.F1) == reflect.TypeOf(f1))    // true
  fmt.Println(reflect.TypeOf((*A).F2) == reflect.TypeOf(f2)) // true
}

返回值

匿名返回值
func inc(a int) int {
  var b int
  defer func() {
    a++
    b++
  }()

  a++
  b = a
  return b
}

func main() {
  var a, b int
  b = inc(a)
  fmt.Println(a, b) // 0, 1
}
before call inc
栈帧
局部变量 a = 0main BP
局部变量 b = 0
inc 返回值 0
inc 参数 a = 0
返回地址main SP
main BP
局部变量 b = 0inc BP

进入 inc 方法后,会将 a 增加 1 并赋值给 b,最后将 b 赋值给返回值

before return
栈帧
局部变量 a = 0main BP
局部变量 b = 0
inc 返回值 1
inc 参数 a = 1
返回地址main SP
main BP
局部变量 b = 1inc BP

在返回 b 之后会执行 defer 匿名函数的内容,将 ab 都增加 1,最后返回:

handle defer
栈帧
局部变量 a = 0main BP
局部变量 b = 0
inc 返回值 = 1
inc 参数 a = 2
返回地址main SP
main BP
局部变量 b = 2inc BP

返回 main 方法后,将返回值赋值给局部变量 b

return main
栈帧
局部变量 a = 0main BP
局部变量 b = 1
inc 返回值 1
inc 参数 a = 2
返回地址main SP
main BP

所以最后输出的 ab01

命名返回值
func inc(a int) (b int) {
  defer func() {
    a++
    b++
  }()

  a++
  b = a
  return b
}

func main() {
  var a, b int
  b = inc(a)
  fmt.Println(a, b) // 0, 2
}
Details
栈帧
局部变量 a = 0main BP
局部变量 b = 0
inc 返回值 0
inc 参数 a = 0
返回地址main SP
main BP
Details
栈帧
局部变量 a = 0main BP
局部变量 b = 0
inc 返回值 1
inc 参数 a = 1
返回地址main SP
main BP
Details
栈帧
局部变量 a = 0main BP
局部变量 b = 0
inc 返回值 2
inc 参数 a = 2
返回地址main SP
main BP
Details
栈帧
局部变量 a = 0main BP
局部变量 b = 2
inc 返回值 2
inc 参数 a = 2
返回地址main SP
main BP

基于寄存器

Reference

 
 comment..
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.0.1
Theme by antfu
2018 - Present © Alomerry Wu