Go 函数调用惯例

nino's blog · · 1119 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

对比C++,Go不支持重载和默认参数,支持不定长变参,多返回值,匿名函数和闭包。

C入栈顺序和返回值

之前有个疑问,为什么Go支持多返回值,而C不行呢。首先回顾一下C函数调用时的栈空间 程序员的自我修养Ch10-2。函数调用时首先参数和返回地址入栈,其次入栈old ebp和需要保存的寄存器,之后是函数内部的局部变量和其他数据。两个指针ebp和esp分别指向返回地址和栈顶。

函数返回值的传递有多种情况。若小于4字节,返回值存入eax寄存器,由函数调用方读取eax。若返回值5到8字节,采用eax和edx联合返回。若大于8个字节,首先在栈上额外开辟一部分空间temp,将temp对象的地址做为隐藏参数入栈。函数返回时将数据拷贝给temp对象,并将temp对象的地址用寄存器eax传出。调用方从eax指向的temp对象拷贝内容。

Go的多返回值实现

C需要多返回值的时候,通常是显示的将返回值存放的地址作为参数传递给函数。Go的调用惯例和C不同,Go把ret1和ret2在参数arg1 arg2之前入栈并保留空位,被调用方将返回值放在这两个空位上。


void f(int arg1, int arg2, int *ret, int *ret2)
func f(arg1, arg2 int) (ret1, ret2 int)

所以无论是Go还是C,为了避免函数返回的对象拷贝,最好不要返回大对象。

匿名函数和闭包

匿名函数可以赋值给变量,作为结构体字段,或者在channel中传递。匿名函数作为返回值赋值给f变量,通过gdb调试时info locals可以查看到f变量的内容是一个地址,info symbol [addr] 可以看到这个地址指向了符号表中的main.test.func1.f符号。返回的匿名函数就是一个保存了匿名函数地址的对象


func test() func(int) int {
	return func(x int) int {
		x += x
		return x
	}
}
f := test()
f(100) // output: 200

闭包是函数式语言的概念。同样闭包是一个对象FuncVal{ func_addr, closure_var_point},它包含了函数地址和引用到的变量的地址。现在有个问题,如果变量x是分配在栈上的,函数test返回以后对应的栈就失效了,test返回的匿名函数中变量x将引用一个失效的位置。所以闭包环境中引用的变量不会在栈上分配。Go编译器通过逃逸分析自动识别出变量的作用域,在堆上分配内存,而不是在函数f的栈上。

逃逸分析可以解释为什么Go可以返回局部变量的地址,而C不行。


func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)
    return func() {
        fmt.Printf("x (%p) = %d\n", &x, x)
    }
}
f := test()
f() // get same output

参考文章 go基础篇 匿名函数和闭包函数

defer 延迟调用

defer的实现

goroutine的控制结构里有一张记录defer表达式的表,编译器在defer出现的地方插入了指令 call runtime.deferproc,它将defer的表达式记录在表中。然后在函数返回之前依次从defer表中将表达式出栈执行,这时插入的指令是call runtime.deferreturn

defer与return

defer在return之前执行的含义是:函数返回时先执行返回值赋值,然后调用defer表达式,最后执行return。以下例子摘自go-internals,总结的都是使用defer的坑。defer确实是在return前调用的,但由于return 语句并不是原子指令,defer被插入到了赋值和ret之间,因此可能有机会改变最终的返回值。


func f() (result int) {
    defer func() {   // result = 0
        result++     // result++
    }()              // return 1
    return 0
}

func f() (r int) {
     t := 5           // r = t = 5
     defer func() {   // t = t + 5 = 10
       t = t + 5      // return 5
     }()
     return t
}

func f() (r int) {
    defer func(r int) {  // r = 1
          r = r + 5      // internal r = 6
    }(r)                 // return 1
    return 1
}

这个现象是在之前做格式化error输出的时候发现的。

defer与闭包

defer调用参数x是在defer注册时求值或复制的,因此以下例子中x在最终调用时仍为10,而由于y是闭包参数,闭包复制的是y变量指针,因此最终y为120,实现了延迟读取。在实际应用中还可以用指针来实现defer的延迟读取。


fund test() {
  x, y := 10, 20
  defer func(i int) {
    fmt.Println("defer:", i, y) // output: 10 120
  }(x)
  
  x += 10
  y += 100
  fmt.Println(x, y) // output: 20 120
}

defer的性能

简单的BenchmarkTest测试发现滥用defer可能会导致性能问题,尤其在大循环中。

参考文章 Go学习笔记


有疑问加站长微信联系(非本文作者)

本文来自:nino's blog

感谢作者:nino's blog

查看原文:Go 函数调用惯例

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077

1119 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传