文章目录

  • GoLang之Function Value、闭包系列三(面试题)
  • 1.题目
  • 2.答案
  • 3.预备知识
  • 4.解析


GoLang之Function Value、闭包系列三(面试题)

注:本文以Go SDK v1.17进行讲解

1.题目

“这是一位小伙伴提供的题目,涉及到对命名返回值、闭包、捕获返回值地址、变量逃逸的理解,不确定是不是面试题,但是应该对解答相关面试题有帮助,所以分享给大家~”

下面的代码输出什么?

package main
func main(){
    x()()
}
func x() (y func()) {
    y = func(){
        println("y")
    }
    return func(){
        println("z")
        y()
    }
}

2.答案

持续输出 z

3.预备知识

这里涉及到函数作为返回值,所以我们先来回忆一下Function Value的相关知识:
在Go语言中,函数是头等对象,可以作为参数、返回值或者赋值给变量,此时它被称为Function Value。
Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体。而这个结构体中存储的才是函数指令入口。

type funcval struct {
    fn uintptr
}

而且,在Go语言中,闭包只是一个有捕获列表的Function Value而已,至于捕获列表中究竟是捕获变量的值还是地址,取决于该变量被赋初始值以后是否又被修改过,修改过就捕获地址。而且捕获局部变量、捕获参数和捕获返回值的处理方式有些许不同。这些内容我们在图解FunctionValue中都介绍过。
有了这些基础知识,我们再来看这道题。

4.解析

我们先来画一下main函数调用函数x的栈帧。main函数没有局部变量,被调用函数也没有参数,所以只有返回值空间有一个y,并且它是一个Function Value,存储了堆上一个runtime.funcval结构体的指针。

go Function value_main函数

Tip:虽然Go1.17新版调用约定使用寄存器传参,但这里我们依然采用之前通过栈来传参的方式绘制栈帧,和寄存器传参道理一样,但画在栈上更方便理解~

然后我们梳理一下函数x,它先给命名返回值y赋初始值,然后下面的return语句又修改了y,并且返回值中有使用到y。所以最终x的返回值y是个闭包,它捕获了返回值的地址,这就不好办了。
因为main函数调用完x后,原本的返回值空间就不再为调用x函数服务了,栈上存储的y可能会被接下来的函数调用覆盖掉,所以,闭包函数的捕获列表不能捕获y的地址,这种情况该怎么处理呢?
编译器会在堆上分配一个y的副本y’,并且为函数x生成一个局部变量py’,这是指向堆上y’的指针,在函数x与返回的闭包函数中都使用这个副本y’,并在函数x返回前,将副本y’拷贝到栈上的返回值y,如下图所示。

go Function value_main函数_02

注意:返回值空间的y和堆上的y’都指向堆上同一个闭包对象,但闭包对象的捕获列表中,捕获的是副本y’的地址。
这就是返回值逃逸的一种场景,实际上编辑器会让返回值的副本逃逸到堆上,因为返回值的类型和存储位置都是不能变的。

所以,最终结果就是,main函数通过y找到堆上的闭包对象,调用闭包函数。
还记得调用闭包函数时,怎么传递捕获列表的地址吗?就是通过寄存器DX,通过DX存储的地址加上偏移就找到了闭包对象的捕获列表。
闭包函数首次执行,输出一个z之后,再次调用y,这个y要通过捕获的变量来定位,实际会调用y’,而y’同样指向这个闭包对象,所以就会出现持续输出字母z的结果。