golang内核系列--深入理解函数闭包

buptbill220 · · 685 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

问题

闭包 是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

比如下面“斐波那契数列”闭包:

func fib() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}

调用如下
f00 := fib()
fmt.Println(f00(), f00(), f00(), f00(), f00())
输出结果是:1 1 2 3 5

golang里是如何做到这种闭包管理的呢?

闭包实现

我们先对闭包分3种场景:

  • 闭包里没有引用环境(变量生命周期很短,调用完即释放)
  • 闭包里引用全局变量(变量生命周期就是全局变量生命周期)
  • 闭包里引用局部变量(变量生命周期长,调用完不释放,下次调用会继续引用)

分别对3种场景以以下代码进行分析:

var y int

// 第一种场景
func fib01() func() int {
	return func() int {
		a, b := 0, 1
		a, b = b, a+b
		return a
	}
}

// 第二种场景
func fib00() func() int {
	return func() int {
		y++
		return y
	}
}

// 第三种场景
func fib3(x int) func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		x++
		return a+x
	}
}

保存上述文件为closure.go,然后在用汇编工具生成汇编代码。汇编参考golang内核系列–深入理解plan9汇编&实践
git:git@github.com:buptbill220/gotls.git
使用如下命名生成汇编

go tool compile -S closure.go  > closure.S

我们打开closure.S找到对应的函数闭包位置

第一种场景fib01



然后找到 "".fib01.func1



闭包在全局区定义到代码段

"".fib01.func1·f SRODATA dupok size=8

我们可以看到:这种情况就是一种普通的函数,直接返回函数地址然后调用



为了帮助理解这个实现,我们手动使用汇编根据一个函数地址调用函数

TEXT ·CallTest(SB),NOSPLIT,$0-8
    MOVQ arg+0(FP), AX
    CALL AX
    RET

func call_test() {
	fmt.Printf("call test")
}

func CallTest(uintptr)

x := call_test
CallTest(**(*(*uintptr))(unsafe.Pointer(&x)))

结果输出:call test

第二种场景fib00



然后找到 “”.fib00.func1



y的位置

"".y SNOPTRBSS size=8

我们可以看到,第二种场景也是返回函数闭包的地址,只是闭包内部访问全局变量,并不做额外的工作。可以和场景1归纳为一类

第三种场景fib3

由于汇编较多,拆为2部分





然后找到 “”.fib3.func1



通过上述分析,我们可以得知,所有引用局部变量,Golang在生成汇编是帮我们在堆上创建该变量的一个拷贝,并把该变量地址和函数闭包组成一个结构体,并把该结构体传出来作为返回值。
结构体形式如下:

type FF struct {
	F unitptr
	B *int
	A *int
	X *int // 如果X是string/[]int,那么这里应该为*string,*[]int
}

这个结构有个特点:结构里变量顺序和函数里引用顺序刚好相反。这是因为Golang为了保持物理地址顺序一致性的结果。

  • 栈的物理空间增长顺序是从大到小,栈里看到地址顺序是x>a>b
  • 而我们引用的是堆上物理空间增长顺序是从小到大,为了保持和栈上物理地址顺序一致,生成的结构顺序就是b、a、x



为了帮助理解这种实现,我们手动把函数闭包转换成结构并输出:

type FF struct {
	F unitptr
	b *int
	a *int
	x *int
}
f := fib3(0)
ptr := *(**FF)(unsafe.Pointer(&f))
fmt.Printf("ptr %v, %d, %d, %d\n", ptr, *ptr.a, *ptr.b, *ptr.x)
fmt.Println(f(), f(), f(), f(), f())
fmt.Printf("ptr %v, %d, %d, %d\n", ptr, *ptr.a, *ptr.b, *ptr.x)

自己手动调试看看

这里还有另一个特点:函数闭包内部本身是通过寄存器来访问引用环境的变量,在闭包调用前会把该结构地址提前放置寄存器(这里放到DX)
我们可以看看闭包调用前的代码:



总结

golang的函数闭包实现主要分为2中场景:

  • 闭包里没有引用环境&获取引用全局变量。这种场景下,其实现就是普通的函数,按照普通的函数调用方式执行闭包调用。
  • 闭包里引用局部变量。这种场景下,才是真正的闭包(函数+引用环境),并且以一个struct{FuncAddr, LocalAddr3, LocalAddr2, LocalAddr1}结构存储该闭包,等到调用闭包时,会把该结构地址提前放置一个寄存器,闭包内部通过该寄存器访问引用环境的变量

有疑问加站长微信联系(非本文作者)

本文来自:知乎专栏

感谢作者:buptbill220

查看原文:golang内核系列--深入理解函数闭包

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

685 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传