闭包
在函数编程中经常用到闭包,闭包是什么?它是怎么产生的及用来解决什么问题呢?先给出闭包的字面定义:闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。这个从字面上很难理解,特别对于一直使用命令式语言进行编程的程序员们。
闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:
- 函数是一等公民(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。
- 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。
在面向对象编程中,我们把对象传来传去,那在函数式编程中,要做的是把函数传来传去,说成术语,把他叫做高阶函数。在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
在函数式编程中,函数是基本单位,是第一型,他几乎被用作一切,包括最简单的计算,甚至连变量都被计算所取代。
闭包小结:
函数只是一段可执行代码,编译后就“固化”了,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数了。在函数式编程语言中,函数是一等公民(First class value):第一类对象,我们不需要像命令式语言中那样借助函数指针,委托操作函数,函数可以作为另一个函数的参数或返回值,可以赋给一个变量。函数可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
)
func adder() func(int) int {
sum := 0
innerfunc := func(x int) int {
sum += x
return sum
}
return innerfunc
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}
|
在这段程序中,函数innerfunc是函数adder的内嵌函数,并且是adder函数的返回值。我们注意到一个问题:内嵌函数innerfunc中引用到外层函数中的局部变量sum,Go会这么处理这个问题呢?先让我们来看看这段代码的运行结果:
1
2
3
4
5
6
7
8
9
10
|
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
|
注意:Go不能在函数内部显式嵌套定义函数,但是可以定义一个匿名函数。如上面所示,我们定义了一个匿名函数对象,然后将其赋值给innerfunc,最后将其作为返回值返回。
当用不同的参数调用adder函数得到(pos(i),neg(i))
函数时,得到的结果是隔离的,也就是说每次调用adder返回的函数都将生成并保存一个新的局部变量sum。其实这里adder函数返回的就是闭包。
这个就是Go中的闭包,一个函数和与其相关的引用环境组合而成的实体。一句关于闭包的名言: 对象是附有行为的数据,而闭包是附有数据的行为。
匿名函数
匿名函数作为返回对象性能上要比正常的函数性能要差。
在 Go 中不支持函数嵌套定义,函数内嵌套函数,必须通过匿名函数的形式。匿名函数在 Go 中是很常见的,比如开启一个 goroutine,通常通过匿名函数。
现在有一个问题,以下代码是闭包吗?
1
2
3
4
5
6
7
8
9
10
11
12
|
package main
import (
"fmt"
)
func main() {
a := 5
func() {
fmt.Println("a =", a)
}()
}
|
如果按照上面网上一般的回答,这不是闭包,因为并没有返回函数。但按照维基百科的定义,这个属于闭包。有没有其他证据呢?
在 Go 语言规范中,关于函数字面值(匿名函数)有这么一句话:
Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
也就是说,函数字面值(匿名函数)是闭包,它们可以引用外层函数定义的变量。
此外,在官方 FAQ 中有这样的说明:
What happens with closures running as goroutines?
例子是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
|
这是 Go 中很常见的代码(很容易写错的),FAQ 称开启 goroutine 的那个匿名函数是一个闭包。
匿名函数实现
前面有提到 Go 里面匿名函数与普通函数区别不大,但是这不大的区别到底在哪里?在这我们用一个简短的小例子来看一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package main
func myFunc(message int) {
println(message)
}
func main() {
f := func(message int) {
println(message)
}
f(0x100)
myFunc(0x100)
}
|
首先我们将上面的代码编译
1
|
go build -gcflags "-N -l -m" -o test
|
生成一个 elf 格式的文件 test。
然后我们通过 go 提供的反汇编工具,反编译我们刚刚生成的 test 文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
$go tool objdump -s "main\.main" ./test
TEXT main.main(SB) /root/data/example/closures/anonymous_func.go
anonymous_func.go:7 0x401040 64488b0c25f8ffffff FS MOVQ FS:0xfffffff8, CX
anonymous_func.go:7 0x401049 483b6110 CMPQ 0x10(CX), SP
anonymous_func.go:7 0x40104d 7637 JBE 0x401086
anonymous_func.go:7 0x40104f 4883ec10 SUBQ $0x10, SP
anonymous_func.go:8 0x401053 488d1d16830800 LEAQ 0x88316(IP), BX
anonymous_func.go:8 0x40105a 48895c2408 MOVQ BX, 0x8(SP)
anonymous_func.go:11 0x40105f 48c7042400010000 MOVQ $0x100, 0(SP)
anonymous_func.go:11 0x401067 488b5c2408 MOVQ 0x8(SP), BX
anonymous_func.go:11 0x40106c 4889da MOVQ BX, DX
anonymous_func.go:11 0x40106f 488b1a MOVQ 0(DX), BX
anonymous_func.go:11 0x401072 ffd3 CALL BX
anonymous_func.go:12 0x401074 48c7042400010000 MOVQ $0x100, 0(SP)
anonymous_func.go:12 0x40107c e87fffffff CALL main.myFunc(SB)
anonymous_func.go:13 0x401081 4883c410 ADDQ $0x10, SP
anonymous_func.go:13 0x401085 c3 RET
anonymous_func.go:7 0x401086 e8b59f0400 CALL runtime.morestack_noctxt(SB)
anonymous_func.go:7 0x40108b ebb3 JMP main.main(SB)
anonymous_func.go:7 0x40108d cc INT $0x3
anonymous_func.go:7 0x40108e cc INT $0x3
anonymous_func.go:7 0x40108f cc INT $0x3
...
|
上面的汇编输出中我们可以看到一共有三次 CALL, 排除调最后那个 runtime 的 CALL ,剩下两次分别对应了匿名函数调用以及正常的函数调用。而两次的区别在于正常的函数是 CALL main.myFunc(SB) , 匿名函数的调用是 CALL BX 。这两种不同的调用方式意味着什么?我们可以通过 gdb 来动态的跟踪这段代码来具体分析一下。
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
|
gdb main
Reading symbols from test...done.
(gdb) b main.main
Breakpoint 1 at 0x401040: file /root/data/example/closures/anonymous_func.go, line 7.
(gdb) r
Starting program: /root/data/example/closures/test
[New LWP 2067]
[New LWP 2068]
[New LWP 2069]
Breakpoint 1, main.main () at /root/data/example/closures/anonymous_func.go:7
7 func main() {
(gdb) l
2
3 func myFunc(message int) {
4 println(message)
5 }
6
7 func main() {
8 f := func(message int) {
9 println(message)
10 }
11 f(0x100)
(gdb) i locals
f = {void (int)} 0xc820039f40
(gdb) x/1xg 0xc820039f40
0xc820039f40: 0x000000c820000180
|
上面在 gdb 里面把断点设置在 main.main 处,然后通过输出当前的环境变量可以看到变量 f。这时候显示 f 指针指向的内存内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
(gdb) b 11
Breakpoint 2 at 0x40105f: file /root/data/example/closures/anonymous_func.go, line 11.
(gdb) c
Continuing.
Breakpoint 2, main.main () at /root/data/example/closures/anonymous_func.go:11
11 f(0x100)
(gdb) i locals
f = {void (int)} 0xc820039f40
(gdb) x/1xg 0xc820039f40
0xc820039f40: 0x0000000000489370
(gdb) i symbol 0x0000000000489370
main.main.func1.f in section .rodata of /root/data/example/closures/test
(gdb) x/2xg 0x0000000000489370
0x489370 <main.main.func1.f>: 0x0000000000401090 0x0000000000441fa0
(gdb) i symbol 0x0000000000401090
main.main.func1 in section .text of /root/data/example/closures/test
|
然后在调用匿名函数 f 的地方再设置一个断点, c 让程序执行到新的断点。再输出 f 指针指向的内存,发现里面的内容已经改变了,输出符号名可以看到符号是 main.main.func1.f, 这个是编译器提我们生成的符号名,然后看一下这个地址指向的内容,会发现 main.main.func1 ,也就是就是我们的匿名函数。接着跟
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
35
|
(gdb) i r
rax 0xc820000180 859530330496
rbx 0x489370 4756336
...
(gdb) disassemble
Dump of assembler code for function main.main:
0x0000000000401040 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000401049 <+9>: cmp 0x10(%rcx),%rsp
0x000000000040104d <+13>: jbe 0x401086 <main.main+70>
0x000000000040104f <+15>: sub $0x10,%rsp
0x0000000000401053 <+19>: lea 0x88316(%rip),%rbx ## 0x489370 <main.main.func1.f>
0x000000000040105a <+26>: mov %rbx,0x8(%rsp)
=> 0x000000000040105f <+31>: movq $0x100,(%rsp)
0x0000000000401067 <+39>: mov 0x8(%rsp),%rbx
0x000000000040106c <+44>: mov %rbx,%rdx
0x000000000040106f <+47>: mov (%rdx),%rbx
0x0000000000401072 <+50>: callq *%rbx
0x0000000000401074 <+52>: movq $0x100,(%rsp)
0x000000000040107c <+60>: callq 0x401000 <main.myFunc>
0x0000000000401081 <+65>: add $0x10,%rsp
0x0000000000401085 <+69>: retq
0x0000000000401086 <+70>: callq 0x44b040 <runtime.morestack_noctxt>
0x000000000040108b <+75>: jmp 0x401040 <main.main>
0x000000000040108d <+77>: int3
0x000000000040108e <+78>: int3
0x000000000040108f <+79>: int3
End of assembler dump.
(gdb) p $rsp
$2 = (void *) 0xc820039f38
(gdb) x/1xg 0xc820039f38
0xc820039f38: 0x0000000000000000
(gdb) ni
0x0000000000401067 11 f(0x100)
(gdb) x/1xg 0xc820039f38
0xc820039f38: 0x0000000000000100
|
输出寄存器里面的值看一下,可以注意到寄存器 rbx 的内存地址是 func1.f 的地址。然后反编译可以看到执行到了 +31 这一行,将常量 0x100 放在 rsp 内指针指向的内存地址。输出 rsp 的内容,然后显示地址指向内存的内容,可以看到是 0x0000000000000000,输入 ni 执行这一行汇编之后再看,就看到内存里面的内容变成了 0x0000000000000100,也就是我们输入常量。
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
35
36
37
|
(gdb) ni
0x000000000040106c 11 f(0x100)
(gdb) ni
0x000000000040106f 11 f(0x100)
(gdb) disassemble
Dump of assembler code for function main.main:
0x0000000000401040 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000401049 <+9>: cmp 0x10(%rcx),%rsp
0x000000000040104d <+13>: jbe 0x401086 <main.main+70>
0x000000000040104f <+15>: sub $0x10,%rsp
0x0000000000401053 <+19>: lea 0x88316(%rip),%rbx ## 0x489370 <main.main.func1.f>
0x000000000040105a <+26>: mov %rbx,0x8(%rsp)
0x000000000040105f <+31>: movq $0x100,(%rsp)
0x0000000000401067 <+39>: mov 0x8(%rsp),%rbx
0x000000000040106c <+44>: mov %rbx,%rdx
=> 0x000000000040106f <+47>: mov (%rdx),%rbx
0x0000000000401072 <+50>: callq *%rbx
0x0000000000401074 <+52>: movq $0x100,(%rsp)
0x000000000040107c <+60>: callq 0x401000 <main.myFunc>
0x0000000000401081 <+65>: add $0x10,%rsp
0x0000000000401085 <+69>: retq
0x0000000000401086 <+70>: callq 0x44b040 <runtime.morestack_noctxt>
0x000000000040108b <+75>: jmp 0x401040 <main.main>
0x000000000040108d <+77>: int3
0x000000000040108e <+78>: int3
0x000000000040108f <+79>: int3
End of assembler dump.
(gdb) ni
0x0000000000401072 11 f(0x100)
(gdb) p $rbx
$5 = 4198544
(gdb) i r
rax 0xc820000180 859530330496
rbx 0x401090 4198544
...
(gdb) x/1xg 0x401090
0x401090 <main.main.func1>: 0xfffff8250c8b4864
|
接着往下执行到 +47 这一行,可以看到 rbx 里面的值在这一行会有变化,ni 执行完这一行,输出寄存器的内容看一下,然后显示 rbx 指向的内存可以看到我们的匿名函数 func1。
现在基本可以理清 Go 里面匿名函数与正常的函数区别,参数的传递区别不大,只是在调用方面,匿名函数需要通过一个包装对象`func1.f`` 来调用匿名函数,这个过程通过 rbx 进行二次寻址来完成调用。理论上,匿名函数也会比正常函数性能要差。
闭包实现
闭包函数携带着定义这个函数的的环境。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
func myFunc() func() int{
foo := 0
return func() int {
foo++
return foo
}
}
func main() {
bar := myFunc()
value_1 := bar()
value_2 := bar()
println(value_1) // 1
println(value_2) // 2
}
|
与分析匿名函数的过程一样,编译然后通过 gdb 来跟踪。
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
35
36
37
38
39
40
41
42
43
44
45
|
$ go build -gcflags "-N -l -m" closure_func.go
## command-line-arguments
./closure_func.go:5: func literal escapes to heap
./closure_func.go:5: func literal escapes to heap
./closure_func.go:4: moved to heap: foo
./closure_func.go:6: &foo escapes to heap
$ gdb closure_func
(gdb) b main.main
Breakpoint 1 at 0x4010d0: file /root/data/example/closures/closure_func.go, line 11.
(gdb) r
Starting program: /root/data/example/closures/closure_func
[New LWP 5367]
[New LWP 5368]
[New LWP 5370]
[New LWP 5369]
Breakpoint 1, main.main () at /root/data/example/closures/closure_func.go:11
11 func main() {
(gdb) i locals
value_2 = 859530428512
value_1 = 0
bar = {void (int *)} 0xc820039f40
gdb 在 main.main 设置断点并输出环境变量可以看到 bar,而且 bar 是一个指针。
(gdb) disassemble
Dump of assembler code for function main.main:
0x00000000004010d0 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x00000000004010d9 <+9>: cmp 0x10(%rcx),%rsp
0x00000000004010dd <+13>: jbe 0x40115c <main.main+140>
0x00000000004010df <+15>: sub $0x20,%rsp
0x00000000004010e3 <+19>: callq 0x401000 <main.myFunc>
=> 0x00000000004010e8 <+24>: mov (%rsp),%rbx
0x00000000004010ec <+28>: mov %rbx,0x18(%rsp)
0x00000000004010f1 <+33>: mov 0x18(%rsp),%rbx
0x00000000004010f6 <+38>: mov %rbx,%rdx
...
(gdb) i r
rax 0x80000 524288
rbx 0xc82000a140 859530371392
...
(gdb) x/2xg 0xc82000a140
0xc82000a140: 0x0000000000401170 0x000000c82000a0b8
(gdb) x/2xg 0x0000000000401170
0x401170 <main.myFunc.func1>: 0x085a8b4810ec8348 0x44c74808245c8948
|
将程序继续向下走到 +24 这一行,然后输出寄存器的信息,能够发现寄存器 rbx 与之前匿名函数的作用类似,都指向了闭包返回对象。里面封装着我们需要用到的匿名函数。可以看到匿名函数作为返回结果,整个调用过程跟是否形成闭包区别不大。那这个区别在哪里呢?
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
35
36
37
38
39
40
41
42
43
44
|
(gdb) disassemble
Dump of assembler code for function main.main:
0x00000000004010d0 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x00000000004010d9 <+9>: cmp 0x10(%rcx),%rsp
0x00000000004010dd <+13>: jbe 0x40115c <main.main+140>
0x00000000004010df <+15>: sub $0x20,%rsp
0x00000000004010e3 <+19>: callq 0x401000 <main.myFunc>
0x00000000004010e8 <+24>: mov (%rsp),%rbx
0x00000000004010ec <+28>: mov %rbx,0x18(%rsp)
0x00000000004010f1 <+33>: mov 0x18(%rsp),%rbx
0x00000000004010f6 <+38>: mov %rbx,%rdx
=> 0x00000000004010f9 <+41>: mov (%rdx),%rbx
0x00000000004010fc <+44>: callq *%rbx
0x00000000004010fe <+46>: mov (%rsp),%rbx
0x0000000000401102 <+50>: mov %rbx,0x10(%rsp)
...
End of assembler dump.
(gdb) ni
0x00000000004010fc 13 value_1 := bar()
(gdb) si
main.myFunc.func1 (~r0=859530371392) at /root/data/example/closures/closure_func.go:5
5 return func() int {
(gdb) disassemble
Dump of assembler code for function main.myFunc.func1:
=> 0x0000000000401170 <+0>: sub $0x10,%rsp
0x0000000000401174 <+4>: mov 0x8(%rdx),%rbx
0x0000000000401178 <+8>: mov %rbx,0x8(%rsp)
0x000000000040117d <+13>: movq $0x0,0x18(%rsp)
0x0000000000401186 <+22>: mov 0x8(%rsp),%rbx
0x000000000040118b <+27>: mov (%rbx),%rbp
...
End of assembler dump.
(gdb) i r
rax 0x80000 524288
rbx 0x401170 4198768
rcx 0xc820000180 859530330496
rdx 0xc82000a140 859530371392
...
(gdb) x/2xg 0xc82000a140
0xc82000a140: 0x0000000000401170 0x000000c82000a0b8
(gdb) x/2xg 0x0000000000401170
0x401170 <main.myFunc.func1>: 0x085a8b4810ec8348 0x44c74808245c8948
(gdb) x/2xg 0x000000c82000a0b8
0xc82000a0b8: 0x0000000000000000 0x3d534e4d554c4f43
|
让程序执行到 +44 行,si 进入到匿名函数内部。在 func1 内部可以看到从 rdx 取数据。输出 rdx 内容,可以看到前面指向匿名函数,而后面则指向另外的内容 0x0000000000000000。
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
(gdb) b 14
Breakpoint 2 at 0x401107: file /root/data/example/closures/closure_func.go, line 14.
(gdb) c
Continuing.
Breakpoint 2, main.main () at /root/data/example/closures/closure_func.go:14
14 value_2 := bar()
14 value_2 := bar()
(gdb) disassemble
Dump of assembler code for function main.main:
0x00000000004010d0 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x00000000004010d9 <+9>: cmp 0x10(%rcx),%rsp
0x00000000004010dd <+13>: jbe 0x40115c <main.main+140>
0x00000000004010df <+15>: sub $0x20,%rsp
0x00000000004010e3 <+19>: callq 0x401000 <main.myFunc>
0x00000000004010e8 <+24>: mov (%rsp),%rbx
0x00000000004010ec <+28>: mov %rbx,0x18(%rsp)
0x00000000004010f1 <+33>: mov 0x18(%rsp),%rbx
0x00000000004010f6 <+38>: mov %rbx,%rdx
0x00000000004010f9 <+41>: mov (%rdx),%rbx
0x00000000004010fc <+44>: callq *%rbx
0x00000000004010fe <+46>: mov (%rsp),%rbx
0x0000000000401102 <+50>: mov %rbx,0x10(%rsp)
=> 0x0000000000401107 <+55>: mov 0x18(%rsp),%rbx
0x000000000040110c <+60>: mov %rbx,%rdx
0x000000000040110f <+63>: mov (%rdx),%rbx
0x0000000000401112 <+66>: callq *%rbx
0x0000000000401114 <+68>: mov (%rsp),%rbx
...
End of assembler dump.
(gdb) ni 3
0x0000000000401112 14 value_2 := bar()
(gdb) disassemble
Dump of assembler code for function main.main:
0x00000000004010d0 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x00000000004010d9 <+9>: cmp 0x10(%rcx),%rsp
0x00000000004010dd <+13>: jbe 0x40115c <main.main+140>
0x00000000004010df <+15>: sub $0x20,%rsp
0x00000000004010e3 <+19>: callq 0x401000 <main.myFunc>
0x00000000004010e8 <+24>: mov (%rsp),%rbx
0x00000000004010ec <+28>: mov %rbx,0x18(%rsp)
0x00000000004010f1 <+33>: mov 0x18(%rsp),%rbx
0x00000000004010f6 <+38>: mov %rbx,%rdx
0x00000000004010f9 <+41>: mov (%rdx),%rbx
0x00000000004010fc <+44>: callq *%rbx
0x00000000004010fe <+46>: mov (%rsp),%rbx
0x0000000000401102 <+50>: mov %rbx,0x10(%rsp)
0x0000000000401107 <+55>: mov 0x18(%rsp),%rbx
0x000000000040110c <+60>: mov %rbx,%rdx
0x000000000040110f <+63>: mov (%rdx),%rbx
=> 0x0000000000401112 <+66>: callq *%rbx
0x0000000000401114 <+68>: mov (%rsp),%rbx
...
End of assembler dump.
(gdb) si
main.myFunc.func1 (~r0=1) at /root/data/example/closures/closure_func.go:5
5 return func() int {
(gdb) disassemble
Dump of assembler code for function main.myFunc.func1:
=> 0x0000000000401170 <+0>: sub $0x10,%rsp
0x0000000000401174 <+4>: mov 0x8(%rdx),%rbx
0x0000000000401178 <+8>: mov %rbx,0x8(%rsp)
0x000000000040117d <+13>: movq $0x0,0x18(%rsp)
0x0000000000401186 <+22>: mov 0x8(%rsp),%rbx
0x000000000040118b <+27>: mov (%rbx),%rbp
...
End of assembler dump.
(gdb) i r
rax 0x80000 524288
rbx 0x401170 4198768
rcx 0xc820000180 859530330496
rdx 0xc82000a140 859530371392
...
(gdb) x/2xg 0xc82000a140
0xc82000a140: 0x0000000000401170 0x000000c82000a0b8
(gdb) x/2xg 0x000000c82000a0b8
0xc82000a0b8: 0x0000000000000001 0x3d534e4d554c4f43
(gdb) i locals
&foo = 0xc82000a0b8
|
设置断点进入到下一次闭包内,输出相同的内容,会发现 rdx 后半段指向的内容发生了变化。通过 i locals 查看环境变量,可以看到 foo 的地址是 0xc82000a0b8 , 跟 rdx 的后半段内容一样。
由此可以判断,闭包返回的包装对象是一个复合结构,里面包含匿名函数的地址,以及环境变量的地址。
易错场景
for range 中使用闭包
1
2
3
4
5
6
7
8
9
|
func main() {
s := []string{"a", "b", "c"}
for _, v := range s {
go func() {
fmt.Println(v)
}()
}
select {} //阻塞模式,保证在协程运行结束之前main函数不退出
}
|
来看看结果:
闭包捕获外部变量相当于引用传递,而非值传递.
每一次引用,捕获变量的地址都是固定的.在执行go协程之前,v的值已经通过最后一次循环成为c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package main
import (
"fmt"
)
func main() {
s := []string{"a", "b", "c"}
for _, v := range s {
x:=v
go func() {
fmt.Println(x)
}()
}
select {} // 阻塞模式,保证在协程运行结束之前main函数不退出
}
|
所以结果当然是:
由于使用了 go 协程,并非顺序输出。
函数列表使用不当
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
)
func test() []func() {
var s []func()
for i := 0; i < 3; i++ {
s = append(s, func() { //将多个匿名函数添加到列表
fmt.Println(&i, i)
})
}
return s //返回匿名函数列表
}
func main() {
for _, f := range test() { //执行所有匿名函数
f()
}
}
|
运行结果:
1
2
3
|
0xc420084008 3
0xc420084008 3
0xc420084008 3
|
for循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量.每次 append 操作仅将匿名函数放入到列表中,但并未执行,并且引用的变量都是 i,随着 i 的改变匿名函数中的 i 也在改变,所以当执行这些函数时,他们读取的都是环境变量 i 最后一次的值。解决的方法就是每次复制变量 i 然后传到匿名函数中,让闭包的环境变量不相同。
解决方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"fmt"
)
func test() []func() {
var s []func()
for i := 0; i < 3; i++ {
x := i //复制变量
s = append(s, func() {
fmt.Println(&x, x)
})
}
return s
}
func main() {
for _, f := range test() {
f()
}
}
|
闭包与结构体方法
如果闭包中传入结构体值,则捕获该值所在的地址,执行结构体方法只执行最后一个结构体值的方法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
}
|
这一版本中只输出了最后一个元素,因为在Goroutine下执行的顺序无法预估。这段中for都执行完了go func(){}
才执行,等到v.print()
执行的时候,v变量已经被迭代到最后一个元素了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
go func() {
v.print()
}()
}
time.Sleep(3 * time.Second)
}
|
因为print需要一个 *field
作为接受者,所以如果直接在一个 field的 instance 上调用print方法,我们使用 v.print()
实际上golang发现 print方法需要的是一个 pointer receiver
的时候,就会把这个调用实际写作 (&v).print()
, 所以在TestClosure
中 v 就是*field
, 所以不需要再去 (&v).print()
这样来调用,而在 TestClosure1
中 v 是 field, 所以需要 (&v).print()
这样来调用。
所以上面这段loop可以转化为下面
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
|
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{{"one"}, {"two"}, {"three"}}
var v field
for i := 0; i < len(data); i++ {
v = data[i]
go func(p *field) { // 因为 print方法定义在 *field 上, 所以这里需要的就是 *field
p.print()
}(&v)
}
time.Sleep(3 * time.Second)
}
|
由于 v 在每次循环中都复用,所以传入每个goroutine的地址其实都是一样的,所以对于
1
2
3
|
go func(){
p.print()
}()
|
这种写法而言,即使是 data := []*field{}
, 传入goroutine中的地址然仍是最后一个元素的地址,不同于 go v.print()
在go routine启动之前就会 evaluate v.print()
, 从而把正确的指针传递给 print函数。
下面这个版本能按序输出。循环中的v只定义了一次,赋值了三次,因为data是指针数组,所以range中传给v的是三个元素的指针,v是指针类型,值各不相同,而在传给Goroutine时,v值已经确定了,所以每次for循环时,go v.print()都把当前循环的值都传去了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
}
|
修改全局变量
若是你对闭包理解了,也可以利用闭包来修改全局变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
)
var x int = 1
func main() {
y := func() int {
x += 1
return x
}()
fmt.Println("main:", x, y)
}
//结果是2,2
|
延迟调用
defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用,
defer 中使用匿名函数依然是一个闭包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import "fmt"
func main() {
x, y := 1, 2
defer func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y) // y 为闭包引用
}(x) // 复制 x 的值
x += 100
y += 100
fmt.Println(x, y)
}
|
输出结果:
总结
上面例子中闭包的使用有点类似于面向对象设计模式中的模版模式,在模版模式中是在父类中定义公共的行为执行序列,然后子类通过重载父类的方法来实现特定的操作,而在Go语言中我们使用闭包实现了同样的效果。
其实理解闭包最方便的方法就是将闭包函数看成一个类,一个闭包函数调用就是实例化一个类(在Objective-c中闭包就是用类来实现的),然后就可以从类的角度看出哪些是“全局变量”,哪些是“局部变量”。例如在第一个例子中,pos和neg分别实例化了两个“闭包类”,在这个“闭包类”中有个“闭包全局变量”sum。所以这样就很好理解返回的结果了。
参考:
https://blog.csdn.net/zhangzhebjut/article/details/25181151
https://www.jianshu.com/p/fa21e6fada70
http://sunisdown.me/closures-in-go.html