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

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结构体的指针。

在这里插入图片描述

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,如下图所示。

在这里插入图片描述

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值