golang 闭包

golang 闭包

实例引入

先来看一段代码,下面的addr函数的返回值是另一个函数,被返回的这个函数中,又对addr函数中的变量进行了累加,然后返回。
在main函数中,使用变量pos接收addr()函数返回的函数对象,然后在for循环中调用5次。

package main

import "fmt"

func addr() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos := addr()
	for i := 0; i < 5; i++ {
		fmt.Println(pos(i))
	}
}

运行结果:

0
1
3
6
10

以上被return的函数就是闭包。

什么是函数闭包

闭包的概念

是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。

简而言之,闭包就是能够读取其他函数内部变量的函数。

闭包的价值

闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。

Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。

闭包的机制

function value

go语言中函数是头等对象,可以作为参数传递,可以做函数返回值,也可以绑定到变量。go语言称这样的参数、返回值或变量为function value (函数变量)。

在这里插入图片描述

函数的指令在编译期间生成。而function value本质上是一个函数指针,但是并不直接指向函数指令入口。而是直接指向一个runtime.funcval结构体。这个结构体里只有一个地址,就是这个函数指令的入口地址。

再来看一个例子:

在这里插入图片描述

函数A被赋值给f1和f2两个变量,这种情况,编译器会做出优化。让f1和f2共用一个funcval结构体。
如果函数A的指令在addr1所指的位置。编译阶段,会在只读数据段分配一个funcval结构体。fn指向函数指令入口,而它本身的起始地址,会在执行阶段赋给f1和f2。 通过f1来执行函数, 就会通过它储存的地址,找到对应的funcval结构体,拿到函数入口地址,然后跳转执行,参数为1,下一步输出为2。f2的调用完全相同。

在这里插入图片描述

既然只要有函数入口地址就能调用,为什么要通过funcval结构体来包装这个地址,然后使用一个二级指针来调用呢?
这里其实就是为了处理闭包的情况。

再来看一种情况:

在这里插入图片描述

函数create的返回值是一个函数,但这个函数内部使用了外部定义的变量c。即使create执行结束,通过f1和f2依然能够正常调用这个闭包函数。并使用定义在create函数内部的局部变量c。
这里符合闭包的定义,通常称这个变量c为捕获变量。
闭包函数的指令自然也在编译阶段生成。但因为每个闭包对象都要保存自己的捕获变量,所以要到执行阶段才创建对应的闭包对象。
到执行阶段,main函数栈帧有两个局部变量,然后是返回值空间。到create函数栈帧这里,有一个局部变量c=2。create函数会在堆上分配一个funcval结构体。
fn指向闭包函数入口,除此之外还有一个捕获列表。这里只捕获一个变量c,然后这个结构的起始地址就被作为返回值写入返回值空间。所以f1被赋值为addr2。
下面再次调用create函数。它就会再次创建一个funccval结构体。同样捕获变量c,然后这个起始地址addr3作为返回值写入。最终f2被赋值为addr3。
通过f1和f2调用闭包函数,就会找到各自对应的funcval结构体。拿到同一个函数入口,但是通过f1调用时要使用addr1指向的捕获列表,而使用f2调用时要使用addr2指向的捕获列表。这就是称闭包为有状态的函数的原因。

在这里插入图片描述

那究竟闭包函数是如何找到对应的捕获列表呢?

go语言中通过一个function value调用函数时,会把对应的funcval结构体地址存入特定寄存器,例如amd64平台使用的是DX寄存器。这样,在闭包函数中,就可以通过寄存器取出funcval结构体的地址,然后加上相应的偏移来找到每一个被捕获的变量。所以go语言中闭包就是有捕获列表的Function Value。而没有捕获列表的Function Value直接忽略这个寄存器的值就好了。

最后来看看捕获列表:

它可不是拷贝变量值这么简单,被闭包捕获的变量要在外层函数与闭包函数中表现一致,好像它们在使用同一个变量。

在这里插入图片描述

为此,go语言的编译器针对不同的情况做了不同的处理。

  • 被捕获的变量除了初始化赋值外,在任何地方都没有被修改过,所以直接拷贝值到捕获列表中就可以了。
  • 但是如果除了初始化赋值外还被修改过,那就要再做细分了。
    • 如果被捕获的是局部变量,而且除了初始化赋值外还被修改过, 那么这个局部变量就会被分配到堆上,是变量逃逸的一种场景。
    • 如果修改并被捕获的是参数,涉及到函数原型,就不能像局部变量那样处理了。参数依然通过调用者栈帧传入。但是编译器会把栈上这个参数拷贝到堆上一份。然后外层函数和闭包函数都使用堆上分配的这一个。
    • 如果被捕获的是返回值,处理方式就又有些不同,调用者栈帧上依然会分配返回值空间。不过闭包的外层函数会在堆上也分配一个。但是在外层函数返回前,需要把堆上的返回值拷贝到栈上的返回值空间。

处理方式虽然多样,但是目标只有一个,就是保持捕获变量在外层函数与闭包函数中的一致性。

参考资料:
https://www.bilibili.com/video/BV1ma4y1e7R5?spm_id_from=333.999.0.0

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值