引
如果我們需要使用循環(huán)從0打印到9,每行一個數(shù),我們可以用下面這樣的Go代碼完成
for i := 0; i < 10; i++ {
fmt.Println(i)
}
得到期望的結(jié)果,如下:
0
1
2
3
4
5
6
7
8
9
但是現(xiàn)實中我們往往需要使用異步并發(fā)處理來提高性能,比如循環(huán)中可能是一個很耗時的邏輯。而這個時候就很容易出現(xiàn)問題了。
協(xié)程引用循環(huán)變量的坑
循環(huán)體中啟動協(xié)程異步執(zhí)行,這個時候就容易出現(xiàn)問題了,比如下面這樣一段代碼就會出現(xiàn)我們不期望的結(jié)果。
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
} ()
}
我們期望他能亂序輸出0到9這幾個數(shù),但是他的執(zhí)行結(jié)果并非如此。實際的執(zhí)行結(jié)果如下:
7
10
10
10
10
10
10
10
10
7
可以看到他的執(zhí)行結(jié)果大家基本都輸出10。其實原因也很容易解釋:
主協(xié)程的循環(huán)很快就跑完了,而各個協(xié)程才開始跑,此時
i的值已經(jīng)是10了,所以各協(xié)程都輸出了10。(輸出7的兩個協(xié)程,在開始輸出的時候主協(xié)程的i值剛好是7,這個結(jié)果每次運行輸出都不一樣)
這是一個初學者很容易出現(xiàn)的問題,還比較隱晦難以發(fā)現(xiàn)。
原因與解決辦法
出現(xiàn)這個問題最主要的原因是Golang中允許啟動的協(xié)程中引用外部的變量。Java對這類問題的解決方式比較合理,它也允許異步任務引用外部變量,但是要求外部變量必須是final或者是effective final的[1]。
for (int i = 0; i < 10; i++) {
final int finalI = i;
new Thread(new Runnable() {
public void run() {
// 這兒要求使用變量finalI,
// 如果使用i,就會報編譯錯誤,
// 而且一般IDE也會提示錯誤,我們很容易發(fā)現(xiàn)。
System.out.println(finalI);
}
})
}
所以Java中只能寫一個臨時變量finalI來供異步任務使用,這樣每個異步任務都會拿到當時i的一個snapshot。
Go代碼也能改成類似的代碼使運行出正確的結(jié)果
for i := 0; i < 10; i++ {
i0 := i
go func() {
fmt.Println(i0)
} ()
}
運行結(jié)果為
1
7
2
9
0
3
4
8
6
5
其實Golang推薦其他更簡潔的寫法
for i := 0; i < 10; i++ {
go func(i0 int) {
fmt.Println(i0)
} (i) //
}
// 或者
for i := 0; i < 10; i++ {
// 這一段代碼相當與下面這樣的一段偽碼
// routine = makeroutine(fmt.Println, i)
// start(routine)
// 于是routine中的i值是一個副本
go fmt.Println(i)
}
這兩個寫法其實與前面java代碼中用臨時變量的原理是一樣的,即變量i已經(jīng)有了一個副本,協(xié)程中針對副本處理。
工具
這個問題Golang雖然沒有在語言層面上像Java一樣要求使用final變量,但是他也提供了一個代碼檢查工具go vet能發(fā)現(xiàn)這個問題:
$ go vet main.go
main.go:24:16: loop variable i captured by func literal
我們可以將這個工具集成到IDE中,讓我們在寫代碼的時候能自動對代碼進行檢查,用于快速發(fā)現(xiàn)這類的問題。
Goland設(shè)置
Goland中可以在 Preferences / Tools / File Watchers中添加一個golangci-lint的工具


有了這樣的設(shè)置之后,后續(xù)編輯代碼的時候,他就能自動檢查出這類問題,提示我們可能存在的問題。
golangci-lint run --disable=typecheck demo
main.go:12:16: loopclosure: loop variable i captured by func literal (govet)
fmt.Println(i)
^
參考信息
https://github.com/golang/go/wiki/CommonMistakes
-
effective final出現(xiàn)與java8,見accessing-members-of-enclosing-class) ?