闭包是函数式语言中的概念。闭包是由函数和与其相关的引用环境组合而成的实体。(即:闭包=函数+引用环境)
1
2
3
4
5
6
7
8
9
10
11
func f(i int) func() int {
return func() int {
i++
return i
}
}
c1 := f(0)
c2 := f(0)
c1() // reference to i, i = 0, return 1
c2() // reference to another i, i = 0, return 1
函数与环境
函数 f
返回了一个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量 i
的,而是引用了它所在的环境(函数f
)中的变量 i
。
c1
与 c2
引用的是不同的环境,在调用 i++
时修改的不是同一个 i
,因此两次的输出都是 1。函数 f
每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。闭包其实不再封闭,全局可见的变量的修改,也会对闭包内的这个变量造成影响。
变量 i
是函数 f
中的局部变量,假设这个变量是在函数 f
的栈中分配的,是不可以的。因为函数 f
返回以后,对应的栈就失效了, f
返回的那个函数中变量 i
就引用一个失效的位置了。所以闭包的环境中引用的变量不能够在栈上分配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func foo1(i *int) func() {
f:= func() {
*i = *i + 1
fmt.Printf("foo1 val = %d\n", *i)
}
return f
}
func foo2(i int) func() {
return func() {
i = i + 1
fmt.Printf("foo2 val = %d\n", i)
}
}
x := 133
f1 := foo1(&x)
f2 := foo2(x)
f1() // foo1 val = 134
f2() // foo2 val = 134
f1() // foo1 val = 135
f2() // foo2 val = 135
x = 233
f1() // foo1 val = 234
f2() // foo2 val = 136
f1() // foo1 val = 235
f2() // foo2 val = 137
foo1(&x)() // foo1 val = 236
foo2(x)() // foo2 val = 237
foo1(&x)() // foo1 val = 237
foo2(x)() // foo2 val = 238
foo2(x)() // foo2 val = 238
定义了x=133
之后,获取得到了 f1=foo1(&x)
和f2=foo2(x)
。这里f1\f2
就是闭包的函数,也就是foo1()\foo2()
的内部匿名函数。而闭包的环境即外部函数foo1()\foo2()
的变量i
。
闭包的延迟绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func foo7(x int) []func() {
var fs []func()
values := []int{1, 2, 3, 5}
for _, val := range values {
fs = append(fs, func() {
fmt.Printf("foo7 val = %d\n", x+val)
})
}
return fs
}
f7s := foo7(11)
for _, f7 := range f7s {
f7()
}
//foo7 val = 16
//foo7 val = 16
//foo7 val = 16
//foo7 val = 16
for-loop声明了一组闭包,其中所有闭包的外部环境是同一组(val,x)变量,在执行闭包(执行 f7())的时候寻找外部环境最新的值(很显然,val的最新的值是5,x的值为11),所以这组闭包的返回值都是 16。
1
2
3
4
5
6
7
8
9
10
func foo0() func() {
x := 1
f := func() {
fmt.Printf("foo0 val = %d\n", x)
}
x = 11
return f
}
foo0()() // 11
这就是闭包的神奇之处,闭包会保存外部引用环境,也就是说,val 这个变量在闭包内的生命周期得到了保证。
Go Routine的延迟绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func foo5() {
values := []int{1, 2, 3, 5}
for _, val := range values {
go func() {
fmt.Printf("foo5 val = %v\n", val)
}()
}
}
foo5()
//foo3 val = 5
//foo3 val = 5
//foo3 val = 5
//foo3 val = 5
其实这个问题的本质同闭包的延迟绑定,或者说,这段匿名函数的对象就是闭包。
逃逸分析 escape analyze
Go 语言能通过 escape analyze 识别出变量的作用域,自动将变量在堆上分配。将闭包环境变量在堆上分配是 Go 实现闭包的基础。
1
2
3
4
5
6
7
8
9
type Cursor struct {
X int64
}
func f() *Cursor {
var c Cursor
c.X = 500
// ...
return &c
}
Cursor 是一个结构体,这种写法在 C 语言中是不允许的,因为变量c
是在栈上分配的,当函数f
返回后c
的空间就失效了。但是在 Go 语言规范中有说明,这种写法在 Go 语言中合法的。语言会自动地识别出这种情况并在堆上分配 c
的内存,而不是函数 f
的栈上。
为了验证这一点,可以观察函数f
生成的汇编代码:
1
2
3
4
5
6
7
8
9
MOVQ $type."".Cursor+0(SB),(SP) // 取变量c的类型,也就是Cursor
PCDATA $0,$16
PCDATA $1,$0
CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor)
PCDATA $0,$-1
MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器
MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500
MOVQ AX,"".~r0+24(FP)
ADDQ $16,SP
识别出变量需要在堆上分配,是由编译器的一种叫 escape analyze 的技术实现的。如果输入命令:
1
go build --gcflags=-m main.go
可以看到输出:
1
2
./main.go:20: moved to heap: c
./main.go:23: &c escapes to heap
表示 c
逃逸了,被移到堆中。escape analyze 可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。
闭包结构体
返回闭包时并不是单纯返回一个函数,而是返回了一个结构体,记录下函数返回地址和引用的环境中的变量地址。