16.9 閉包和協程的使用
請看下面代碼:
package main
import (
"fmt"
"time"
)
var values = [5]int{10, 11, 12, 13, 14}
func main() {
// 版本A:
for ix := range values { // ix是索引值
func() {
fmt.Print(ix, " ")
}() // 調用閉包打印每個索引值
}
fmt.Println()
// 版本B: 和A版本類似,但是通過調用閉包作爲一個協程
for ix := range values {
go func() {
fmt.Print(ix, " ")
}()
}
fmt.Println()
time.Sleep(5e9)
// 版本C: 正確的處理方式
for ix := range values {
go func(ix interface{}) {
fmt.Print(ix, " ")
}(ix)
}
fmt.Println()
time.Sleep(5e9)
// 版本D: 輸出值:
for ix := range values {
val := values[ix]
go func() {
fmt.Print(val, " ")
}()
}
time.Sleep(1e9)
}
/* 輸出:
0 1 2 3 4
4 4 4 4 4
1 0 3 4 2
10 11 12 13 14
*/
版本A調用閉包5次打印每個索引值,版本B也做相同的事,但是通過協程調用每個閉包。按理說這將執行得更快,因爲閉包是併發執行的。如果我們阻塞足夠多的時間,讓所有協程執行完畢,版本B的輸出是:4 4 4 4 4
。爲什麼會這樣?在版本B的循環中,ix
變量
實際是一個單變量,表示每個數組元素的索引值。因爲這些閉包都只綁定到一個變量,這是一個比較好的方式,當你運行這段代碼時,你將看見每次循環都打印最後一個索引值4
,而不是每個元素的索引值。因爲協程可能在循環結束後還沒有開始執行,而此時ix
值是4
。
版本C的循環寫法纔是正確的:調用每個閉包是將ix
作爲參數傳遞給閉包。ix
在每次循環時都被重新賦值,並將每個協程的ix
放置在棧中,所以當協程最終被執行時,每個索引值對協程都是可用的。注意這裏的輸出可能是0 2 1 3 4
或者0 3 1 2 4
或者其他類似的序列,這主要取決於每個協程何時開始被執行。
在版本D中,我們輸出這個數組的值,爲什麼版本B不能而版本D可以呢?
因爲版本D中的變量聲明是在循環體內部,所以在每次循環時,這些變量相互之間是不共享的,所以這些變量可以單獨的被每個閉包使用。