Golang知识点六、闭包

Golang知识点六、闭包

  从本篇文章开始,记录闭包相关的知识点。闭包相关的内容包括两个部分,分别是funcval结构体和闭包结构两部分。

1. funcval结构体

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

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

	type funcval struct {
		fn uintptr
	}
	func A(i int) {
		i++
		fmt.Println(i)
	}
	func B() {
		f1 := A
		f1(1)
	}
	func C() {
		f2 := A
		f2(1)
	}

  函数A被赋值给f1和f2两个变量,这种情况下,编译器会做出优化,让f1和f2共用一个funcval结构体,假设编译后A的代码段入口地址为addr1,编译阶段会在只读数据段addr2处分配一个funcval结构体,其中fn指向函数A入口地址。函数函数指向阶段,addr2会被赋值给f1和f2,通过f1执行函数,就会通过它存储的地址找到addr2处的funcval结构体,拿到函数入口地址addr1并跳转执行。

  为什么要通过funcval结构体来包装这个地址,然后用一个二级指针调用呢? 答案是,为了处理闭包的情况。

2. 闭包结构

  闭包的形成,有两个关键点分别是,(1)自由变量:在函数外部定义,在函数内被引用。(2)脱离了形成闭包的上下文,闭包也能照常使用这些自由变量。

2.1. 闭包函数

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

  函数create的返回值是一个函数,在返回函数的内部,使用了外部定义的变量c。即使create执行结束,通过f1和f2依然可以正常调用这个闭包函数,并使用定义在create内的局部变量c。通常称变量c为捕获变量。

  分析执行过程,执行到f1 := create()由于每个闭包对象都要保存自己的捕获变量,所以要到执行阶段才创建对应的闭包对象。 create函数会在地址为addr2的堆上分配一个funcval结构体,指向闭包函数入口;另外,堆上还有一个捕获列表(这里只捕获一个变量c),addr2就作为返回值写入main函数栈帧返回值空间。

  执行到f2 := create()create函数就会在堆上addr3处再次创建一个funcval结构体和一个捕获变量,之后将addr3作为返回值,写入main函数栈帧返回值空间。

  f1被赋值为addr2,f2被赋值为addr3。通过f1和f2调用闭包函数,就会找到各自对应的funcval结构体,拿到同一个函数入口。通过f1调用时要使用addr2处的捕获列表,通过f2调用时,要使用addr3处的捕获列表。正因为如此,也称闭包为有状态的函数。

  闭包函数如何找到对应的捕获列表呢?Golang中通过一个funcval调用函数时,会把对应funcval结构体地址存入特定寄存器(例如ARM64平台使用的DX寄存器),这样就可以通过寄存器取出funcval结构体地址,然后加上偏移找到每个捕获的变量。简单来说,闭包就是有捕获列表的funcval,没有捕获列表的funcval,直接忽略这个寄存器的值就行。

2.2. 捕获列表

  捕获列表不是拷贝变量值这么简单,被闭包捕获的变量,要在外层函数和闭包函数中表现一致。为此,Go语言编译器针对不同情况,做了不同处理。

  最简单的例子就是上面的示例,除了初始化赋值,再没有改变过,所以直接拷贝值到捕获列表中就行了。如果捕获变量除了初始化赋值还被修改过,就不能直接拷贝值了。闭包所导致的局部变量堆分配,也是变量逃逸的一种场景。

	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]()
		}
	}

  变量逃逸: 局部变量被闭包捕获,会改为堆分配,栈上只存堆上的地址,捕获列表也存堆上地址,这样就能保证捕获变量在外层函数和闭包函数中的一致性。

  参数堆分配: 如果修改并被捕获的是参数,涉及到函数原型,就不能像局部变量那样处理了。参数依然通过调用者栈帧传入,但是编译器会把栈上这个参数拷贝到堆上一份,然后外层函数和闭包函数都使用堆上分配的这一个。

  返回值: 如果被捕获的是返回值,调用者栈帧上依然会分配返回值空间。闭包的外层函数会再堆上也分配一个,外层函数和闭包函数都使用堆上这一个。但是,在外层函数返回前,需要把堆上返回值拷贝到栈上的返回值空间。

  处理方式虽然不同,但是目标是一致的,即保持捕获变量在外层函数和在闭包函数中的一致性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值