一個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
語言中的閉包,對于其他語言,應該也适用。