闭包的实现
stringslice
闭包不仅仅是一种语法特性,在实现上也有其特殊之处。它“捕获”外层函数的局部变量,在外层函数返回之后,被捕获的变量不会销毁,而且也再不能被直接访问,只能通过闭包函数来使用,从而将一个函数和一组变量打包在一起,成为一个对象,也就是闭包对象。
Go语言实现了闭包,但是没有将其称为闭包,而是称为匿名函数。我们来看如下示例:
示例1
func getCounter() func(int) int {
c := 0
return func(i int) int {
c += i
return c
}
}
getCountercgetCounter
runtime.funcval
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
runtime.funcval
fnfn
ccc
// 在funcval尾部追加捕获列表
type funcval1 struct {
runtime.funcval
c *int
}
// 闭包函数被编译为独立的函数,通过this指针访问捕获列表
func getCounter.func1(this *runtime.funcval, i int) int {
f1 := (*funcval1)(this)
*f1.c += i
return *f1.c
}
// 所有闭包和函数变量都是*runtime.funcval,只是捕获列表不同
func getCounter() *runtime.funcval {
c := new(int)
f := new(funcval1)
f.fn = &getCounter.func1
f.c = c
return &f.funcval
}
// 调用闭包函数时
f := getCounter()
f.fn(f, 10)
this
// 闭包函数被编译为独立的函数,RDX寄存器用作this指针
func getCounter.func1(i int) int {
f1 := (*funcval1)(RDX)
*f1.c += i
return *f1.c
}
// 调用闭包函数时
f := getCounter()
RDX = f
f.fn(10)
捕获值?捕获地址?
关于闭包捕获外层变量最容易让人疑惑的地方,就是捕获值还是捕获地址。下面我们用几个示例来演示何时捕获值,何时捕获地址,同时提出一种检验方法。
检验方法
// 助手函数
func helper(arg int) {
addr := uintptr(unsafe.Pointer(&arg))
fmt.Printf(“post: %#x\n”, addr)
}
// 为外层函数添加一个占位参数
func getCounter(dummy int) func(int) int {
c := 0
// 添加如下代码块
addr := uintptr(unsafe.Pointer(&dummy))
fmt.Printf(“pre: %#x\n”, addr)
addr = uintptr(unsafe.Pointer(&c))
fmt.Printf(“addr: %#x\n”, addr)
helper(0)
return func(i int) int {
c += i
return c
}
}
上述检验方法的思路是:1)运行时栈向下增长;2)函数的参数通过运行时栈传递;3)函数的局部变量在运行时栈上分配,且在参数之后。
cc
ccc
那么什么时候需要捕获地址呢?我们用一种直观的方法按步分析:1)被捕获的变量出现在两个或更多个函数的作用域中,包括外层函数和一个或多个闭包函数;2)如果在任何一个作用域中,被捕获变量都不曾被修改过(外层函数中赋初始值不算修改),那么所有闭包函数捕获值即可;3)如果被捕获变量在任何一个作用域内被修改,那么变量需要分配在堆上,所有闭包捕获地址。
c
捕获值
// 变量不曾被修改,捕获值
func getf() (f1, f2 func() int) {
a := 10
f1 := func() int {
return a
}
f2 := func() int {
return a
}
return f1, f2
}
捕获地址
// 变量在外层函数被修改,堆分配,捕获地址
func getf() func() int {
a := 10
f := func() int {
return a
}
a = 20
return f
}
捕获循环变量地址
// 循环变量在外层函数被修改,堆分配,捕获地址
func getfs(c int) (fs []func() int) {
for i := 0; i < c; i++ {
f := func() int {
return i
}
fs = append(fs, f)
}
return
}
函数变量
runtime.funcvalfuncval
// 具名函数
func add(a, b int) int {
return a + b
}
f := add
// 函数变量
// f := new(runtime.funcval)
// f.fn = &add
f(10, 20)
// 调用操作
// RDX = f
// f.fn(10, 20)
addfuncvaladd
funcval
打印地址
uintptrfmt.Printf(“%p”, &a)
fmt.Printf
// 变量在闭包函数中被修改,捕获地址
func getf() func() int {
a := 0
return func() int {
a++
return a
}
}
f1 := getf()
f2 := getf()
fmt.Printf(“%p, %p\n”, f1, f2)
funcvalfmt.Printff1f2funcval.fnfuncval
funcval
f1 := getf()
f2 := getf()
a1 := *(*uintptr)(unsafe.Pointer(&f1))
a2 := *(*uintptr)(unsafe.Pointer(&f2))
fmt.Printf(“%#x, %#x\n”, a1, a2)
相等性
nil
funcval.fn
struct
funcval
// 没有捕获列表
func getf() func() int {
return func() int {
return 10
}
}
f1 := getf()
f2 := getf()
a1 := *(*uintptr)(unsafe.Pointer(&f1))
a2 := *(*uintptr)(unsafe.Pointer(&f2))
fmt.Printf(“%#x, %#x\n”, a1, a2)
funcvalgetf()
小结
本文分析了函数变量的底层结构,探讨了捕获列表何时捕获值、何时捕获地址。最后作为总结,让我们手动构造一个函数变量:
// 变量在闭包函数中被修改,捕获地址
func getf() func() int {
a := 0
return func() int {
a++
return a
}
}
type funcval struct {
fn uintptr
aa *int
}
// 利用逃逸分析实现堆分配
func newfv() *funcval {
return new(funcval)
}
func newi() *int {
return new(int)
}
// 通过f1取得函数实际地址
f1 := getf()
var f2 func() int
fv := newfv()
fv.aa = newi()
fv.fn = **(**uintptr)(unsafe.Pointer(&f1))
*(**funcval)(unsafe.Pointer(&f2)) = fv
fmt.Printf(“%d, %d\n”, f1(), f2())
funcvalintf2