原文連接:Go 語言閉包詳解html
什麼是閉包?閉包是由函數和與其相關的引用環境組合而成的實體。golang
下面就來經過幾個例子來講明 Go 語言中的閉包以及由閉包引用產生的問題。閉包
在說明閉包以前,先來了解一下什麼是函數變量。app
在 Go 語言中,函數被看做是第一類值,這意味着函數像變量同樣,有類型、有值,其餘普通變量能作的事它也能夠。函數
func square(x int) {
println(x * x)
}
複製代碼
square(1)
s := square
;接着能夠調用這個函數變量:s(1)
。 注意:這裏 square
後面沒有圓括號,調用纔有。nil
的函數變量會致使 panic。nil
,這意味着它能夠跟 nil
比較,但兩個函數變量之間不能比較。如今開始經過例子來講明閉包:post
func incr() func() int {
var x int
return func() int {
x++
return x
}
}
複製代碼
調用這個函數會返回一個函數變量。ui
i := incr()
:經過把這個函數變量賦值給 i
,i
就成爲了一個閉包。spa
因此 i
保存着對 x
的引用,能夠想象 i 中有着一個指針指向 x 或 i 中有 x 的地址。指針
因爲 i
有着指向 x
的指針,因此能夠修改 x
,且保持着狀態:code
println(i()) // 1
println(i()) // 2
println(i()) // 3
複製代碼
也就是說,x
逃逸了,它的生命週期沒有隨着它的做用域結束而結束。
可是這段代碼卻不會遞增:
println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
複製代碼
這是由於這裏調用了三次 incr()
,返回了三個閉包,這三個閉包引用着三個不一樣的 x
,它們的狀態是各自獨立的。
如今開始經過例子來講明由閉包引用產生的問題:
x := 1
f := func() {
println(x)
}
x = 2
x = 3
f() // 3
複製代碼
由於閉包對外層詞法域變量是引用的,因此這段代碼會輸出 3。
能夠想象 f
中保存着 x
的地址,它使用 x
時會直接解引用,因此 x
的值改變了會致使 f
解引用獲得的值也會改變。
可是,這段代碼卻會輸出 1:
x := 1
func() {
println(x) // 1
}()
x = 2
x = 3
複製代碼
把它轉換成這樣的形式就容易理解了:
x := 1
f := func() {
println(x)
}
f() // 1
x = 2
x = 3
複製代碼
這是由於 f
調用時就已經解引用取值了,這以後的修改就與它無關了。
不過若是再次調用 f
仍是會輸出 3,這也再一次證實了 f
中保存着 x
的地址。
能夠經過在閉包內外打印所引用變量的地址來證實:
x := 1
func() {
println(&x) // 0xc0000de790
}()
println(&x) // 0xc0000de790
複製代碼
能夠看到引用的是同一個地址。
接下來在三個例子中說明由循環內的閉包引用所產生的問題:
for i := 0; i < 3; i++ {
func() {
println(i) // 0, 1, 2
}()
}
複製代碼
這段代碼至關於:
for i := 0; i < 3; i++ {
f := func() {
println(i) // 0, 1, 2
}
f()
}
複製代碼
每次迭代後都對 i
進行了解引用並使用獲得的值且再也不使用,因此這段代碼會正常輸出。
正常代碼:輸出 0, 1, 2:
var dummy [3]int
for i := 0; i < len(dummy); i++ {
println(i) // 0, 1, 2
}
複製代碼
然而這段代碼會輸出 3:
var dummy [3]int
var f func() for i := 0; i < len(dummy); i++ {
f = func() {
println(i)
}
}
f() // 3
複製代碼
前面講到閉包取引用,因此這段代碼應該輸出 i 最後的值 2 對吧?
不對。這是由於 i
最後的值並非 2。
把循環轉換成這樣的形式就容易理解了:
var dummy [3]int
var f func() for i := 0; i < len(dummy); {
f = func() {
println(i)
}
i++
}
f() // 3
複製代碼
i
自加到 3 纔會跳出循環,因此循環結束後 i
最後的值爲 3。
因此用 for range
來實現這個例子就不會這樣:
var dummy [3]int
var f func() for i := range dummy {
f = func() {
println(i)
}
}
f() // 2
複製代碼
這是由於 for range
和 for
底層實現上的不一樣。
var funcSlice []func() for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
println(i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 3, 3, 3
}
複製代碼
輸出序列爲 3, 3, 3。
看了前面的例子以後這裏就容易理解了: 這三個函數引用的都是同一個變量(i
)的地址,因此以後 i
遞增,解引用獲得的值也會遞增,因此這三個函數都會輸出 3。
添加輸出地址的代碼能夠證實:
var funcSlice []func() for i := 0; i < 3; i++ {
println(&i) // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
funcSlice = append(funcSlice, func() {
println(&i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
}
複製代碼
能夠看到三個函數引用的都是 i
的地址。
j := i
,且把以後對 i
的操做改成對 j
操做。i := i
。注意:這裏短聲明右邊是外層做用域的 i
,左邊是新聲明的做用域在這一層的 i
。原理同上。這至關於爲這三個函數各聲明一個變量,一共三個,這三個變量初始值分別對應循環中的 i
而且以後不會再改變。
var funcSlice []func() for i := 0; i < 3; i++ {
func(i int) {
funcSlice = append(funcSlice, func() {
println(i)
})
}(i)
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 0, 1, 2
}
複製代碼
如今 println(i)
使用的 i
是經過函數參數傳遞進來的,而且 Go 語言的函數參數是按值傳遞的。
因此至關於在這個新的匿名函數內聲明瞭三個變量,被三個閉包函數獨立引用。原理跟第一種方法是同樣的。
這裏的解決方法能夠用在大多數跟閉包引用有關的問題上,不侷限於第三個例子。