GoLang之Function Value、闭包系列一(书面版)

GoLang之Function Value、闭包系列一(书面版)

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

1.Function Value

函数,在GO语言中属于头等对象,可以被当作参数传递、也可以作为函数返回值、绑定到变量。Go语言称这样的参数、返回值和变量为“Function Value”。
Function Value本质上是一个指针,却不直接指向函数指令入口,而是指向runtime.funcval结构体。

//runtime/runtime2.go
type funcval struct {
	fn uintptr
	// variable-size, fn-specific data here
}

这个结构体从定义上看只有一个地址,这个地址才是函数的指令入口。所以,一个Function Value是以下图所示形式存在的。

在这里插入图片描述

2.闭包

维基百科:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。

在这里插入图片描述

所以像下面这个例子:

func create() func(){
    c := 2
    return func(){
        fmt.Println(c)
    }
}
func main(){
    f1 := create()
    f2 := create()
    f1()
    f2()
}

create函数的返回值是一个函数,并且引用了其外层函数定义的局部变量c;而且,即便create函数结束,依然可以通过f1和f2正常执行这个函数并使用定义在create内部的变量c。所以这个返回值符合闭包的定义,而这个自由变量c,通常被称为“捕获变量”。

虽然create函数的返回值函数形成闭包,但是Go语言里并没有把闭包从Function Value中特别区分出来。在Go语言中闭包只是拥有一个或多个捕获变量的Function Value而已。这些捕获变量就是它的捕获列表,就放在对应的funcval结构体的后面。所以上例中,f1和f2的内存布局如下图所示。

在这里插入图片描述

每个闭包对象都是一个Function Value,但是各自持有自己的捕获列表,这也是称闭包为“有状态的函数”的原因。

3.调用

通过FunctionValue调用函数时,会把对应的funcval结构体地址存入特定寄存器,例如amd64平台使用的是DX寄存器。
继续使用闭包的示例,通过f1调用闭包函数时,会把f1存储的funcval结构体地址存入寄存器DX,这样在闭包函数的指令中就可以通过这个寄存器存储的地址加上8字节的偏移,就找到f1的捕获变量了。

在这里插入图片描述

同样的,通过f2调用闭包函数时,会把f2存储的funcval结构体地址存入寄存器,闭包函数执行时找到的就是f2的捕获变量了。

在这里插入图片描述

如果是没有捕获列表的Function Value,直接忽略这个寄存器即可。通过这样的方式,Go语言实现了对Function Value的统一调用。对Function Value有了大致了解,就可以关注一些细节了。

4.静态分配

对于没有捕获列表的Function Value,如果多个变量关联到同一个函数,编译器会做出优化,让它们共用一个funcval结构体。

func A(i int) {
    i++
    fmt.Println(i)
}  
func B(){
    f1 := A
    f1(1)
}
func C(){
    f2 := A
    f2(1)
}

像上面这种情况,编译阶段会创建一个funcval结构体放到只读数据段,而执行阶段,f1和f2都会使用它.

在这里插入图片描述

5.捕获列表

5.1介绍

因为捕获列表需要由闭包对象各自持有,所以有捕获列表的Function Value要到执行阶段才会在堆上分配对应的funcval结构体以及捕获列表空间。但是,捕获列表里存什么?直接拷贝捕获变量值吗?才没有那么简单。

闭包捕获的变量要在闭包函数和外层函数中表现一致,如果单纯值拷贝,就无法保证这一点。所以编译器针对捕获变量的不同情况分别做出了不同的处理。

1.捕获变量除了初始化赋值外在任何地方都没有被修改过,那就可以直接拷贝值,因为它不会再变化。
2.捕获变量除了初始化赋值外,还被修改过,就要再细分了。

5.2case[01]捕获局部变量

func create() (fs [2]func()){
    for i := 0; i < 2; i++ {
        fs[i] = func(){
            fmt.Println(i)
        }    
    }
    return
}
func main() {
    fs := create()
    for i := 0; i < len(fs); i++ {
        fs[i]()
    }
}

这个例子中,被捕获的是局部变量i,并且除了初始化赋值外还被修改过,所以局部变量i改为堆分配,栈上只存一个地址。我们把闭包函数指令入口地址记为addrf。

在这里插入图片描述

main函数栈帧中,局部变量fs,以及create函数的返回值都是长度为2的Function Value型数组。在create函数栈帧中,局部变量i被闭包捕获,分配到了堆上,在栈上的局部变量空间只存储它在堆上的地址。在for循环执行以前,i等于0。

在这里插入图片描述

在堆上创建一个funcval结构体,指向闭包函数入口,并且在捕获列表中存储捕获变量i的地址,这样就可以和外层函数使用同一个变量了。返回值数组第一个元素存储这一次创建的funcval结构体地址,第一次for循环结束后,i自增1。

在这里插入图片描述

第二次for循环再次创建一个funcval结构体,存储捕获变量i的地址,而funcval结构体本身的地址作为返回值的第二个元素。第二次循环结束,i再次自增1。

在这个示例中,为了让被捕获的局部变量在闭包函数和外层函数中保持一致,本该在栈上分配的局部变量被分配到堆上,这其实也是“变量逃逸”的一种场景。

5.3case[02]捕获参数

如果是参数被捕获,那么调用者依然从栈上传递参数,但是被调用函数会把它拷贝到堆上一份,然后和闭包函数都使用堆上分配的那一个。

在这里插入图片描述

5.4case[03]捕获返回值

如果是返回值被捕获,那么处理方式就又有些不同了。返回值空间依然由调用者在栈上分配,但是被调用函数(闭包的外层函数)会在堆上也分配一个,并且与闭包函数都使用堆上这一个。但是,在外层函数返回前要把堆上的返回值拷贝到栈上那一个。

在这里插入图片描述

6.关键点

(1)Go语言里Function Value本质上是指向funcval结构体的指针;
(2)Go语言里闭包只是拥有捕获列表的Function Value;
(3)捕获变量在外层函数与闭包函数中要保持一致。

  • 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、付费专栏及课程。

余额充值