闭包的实现

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