一个Go闭包引发的血案
今天群里有人问了个问题,说是以下代码会输出什么:
func main() {
functions := make([]func(),)
for i:; i; i++ {
functions[i] = func() {
fmt.Println(i)
}
}
functions]()
functions]()
functions]()
}
群里有人说是
0 1 2
,也有人说是
3 3 3
。一看这代码就知道是考
闭包
的,一开始我也以为是输出
0 1 2
,运行了一遍后发现不对,输出的是
3 3 3
。
错就错在,我误以为,在表达式:
functions[i] = func() {
fmt.Println(i)
}
中,等号右边的闭包里面,每次循环都把变量
i
的当前值保存下来,因此,最后的输出是
0 1 2
。
这个想法很荒谬,该闭包并没形式参数,里面也没有声明变量,凭什么把每次循环中变量
i
的值保存下来。
关于原因:群里有人说是
作用域
的问题。突然想起之前看过的一本书(记不清讲得是
Go
还是
Python
了),也仔细讲过闭包这一块的内容,但是具体如何说的记不清楚了。
经过一番研究,下面是我自己的理解:
1. 首先看如何输出 0 1 2
,然后分析为什么会输出 0 1 2
。
0 1 2
0 1 2
方法1——通过循环中的临时变量
for i:; i; i++ {
i := i
functions[i] = func() {
fmt.Println(i)
}
}
分析:增加了一个临时变量
i
,注意,
fmt.Println(i)
中的
i
是表达式
i := i
左边那个
i
,而不是
for
循环计数器
i
。 每次循环中,
fmt.Println(i)
中的
i
指向的都是新创建的、不同的
i
。该代码可以保证后续输出的是
0 1 2
。
示意图如下:
方法2——通过高阶函数
for i:; i; i++ {
functions[i] = (func(i int) func() {
return func() {
fmt.Println(i)
}
})(i)
}
分析: 出现了两个闭包,其中带参数的闭包,这里称为外闭包,外闭包返回的闭包,称为内闭包。在
functions[i]
右侧的表达式中,外闭包会立即执行,传入了变量
i
作为外闭包的实参,因次外闭包内部会创建一个变量
i
,该变量
i
的值会等于每次
for
循环中的变量
i
的值,
return func()...
返回的闭包中
fmt.Println(i)
所引用的变量
i
,是外闭包的内部变量
i
,而外闭包每次循环中都会创建并运行。所以呢,也是可以输出
0 1 2
。
2. 再回来看下原来的代码(只关注 for
循环):
for
for i:; i; i++ {
functions[i] = func() {
fmt.Println(i)
}
}
for i:=0; i<3; i++
的
i:=0
这个表达式,会创建一个变量
i
。
functions[i]
右侧表达式中的
fmt.Println(i)
,在每次循环中,引用的都是
for
循环初始化中创建的变量
i
(只创建了一次)。循环结束后,
i
的值变为3,因此最终是输出的结果是
3 3 3
。
示意图如下:
3.总结
对于循环语句中创建的闭包(或是其它情况下创建的闭包),需要明确两点:
- 闭包中所引用的变量到底是哪个变量,要考虑到变量的遮蔽效应。特别注意:闭包如果引用了在循环语句中的声明的变量,需要明白这些变量在每次循环中都会创建,每次都是不一样的变量(分配到不同的内存地址)
- 闭包对外部的变量只是引用,而不是复制,不会创建副本,当闭包中涉及该变量的表达式执行时,还得去外面找这个变量。
闭包是能够捕捉外部变量的匿名函数
。这个
捕捉
,并不是复制,而是 “引用” ,即指向该变量的地址。例如:对本文开篇的那段代码稍微修改下:
func main() {
functions := make([]func(),)
var i int
for i; i; i++ {
functions[i] = func() {
fmt.Println(i)
}
}
i =
functions]()
functions]()
functions]()
}
输出结果是:
10 10 10
。变量
i
的值在创建闭包之后改变了,闭包输出的值也随之改变。
被闭包捕捉的变量因为多了一个对象引用它,只要闭包还有效,被捕捉变量所占据的内存就不会被回收,虽然它的作用域不是全局的。只要闭包活着,闭包所引用的变量就还活着。
该文章虽然分析的是
Go
语言中的闭包,对于其他语言,应该也适用。