一个Go闭包引发的血案
今天群里有人问了个问题,说是以下代码会输出什么:
func main() {
functions := make([]func(), 3)
for i:=0; i<3; i++ {
functions[i] = func() {
fmt.Println(i)
}
}
functions[0]()
functions[1]()
functions[2]()
}
群里有人说是 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
。
方法1——通过循环中的临时变量
for i:=0; i<3; 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:=0; i<3; 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 i:=0; i<3; 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(), 3)
var i int
for i=0; i<3; i++ {
functions[i] = func() {
fmt.Println(i)
}
}
i = 10
functions[0]()
functions[1]()
functions[2]()
}
输出结果是:10 10 10
。变量i
的值在创建闭包之后改变了,闭包输出的值也随之改变。
被闭包捕捉的变量因为多了一个对象引用它,只要闭包还有效,被捕捉变量所占据的内存就不会被回收,虽然它的作用域不是全局的。只要闭包活着,闭包所引用的变量就还活着。
该文章虽然分析的是Go
语言中的闭包,对于其他语言,应该也适用。