Go語言使用range複用臨時變數

2020-07-16 10:05:09
在開始本節的講解之前,大家先來看一段簡單的程式碼:
package main
import "sync"
func main () {
    wg := sync.WaitGroup{}
    si := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for i := range si {
        wg.Add (i)
        go func () {
            println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}
執行結果:

9
9
9
9
9
9
9
9
9
9

程式結果並沒有如我們預期一樣遍歷切片,而是全部列印 9,有兩點原因導致這個問題:
  • for range 下的疊代變數 i 的值是共用的。
  • main 函數所在的 goroutine 和後續啟動的 goroutines 存在競爭關係。

使用 go run -race 來看一下資料競爭情況:

#CGO ENABLED=l go run - race src/c7_2_la.go
WARNING: DATA RACE
Read at 0x00c4200140b8 by goroutine 13:
    main.main.funcl()
        /project/go/src/gitbook/gobook/chapter7/src/c7_2_la.go:14 +0x38
Previous write at 0x00c4200140b8 by main goroutine:
    main.main ()
        /project/go/src/gitbook/gobook/chapter7/src/c7_2_la.go:11 +0xdf
Goroutine 13 (running) created at:
    main.main ()
        /project/go/src/gitbook/gobook/chapter7/src/c7_2_la.go:l3 +0xl35
=================
9
9
9
9
9
9
9
9
9
9
Found 1 data race(s)
exit status 66

可以看到 Goroutine 13 和 main goroutine 存在資料競爭,更進一步證實了 range 共用臨時變數,range 在疊代寫的過程中,多個 goroutine 並行地去讀。

正確的寫法是使用函數引數做一次資料複製,而不是閉包。範例如下:
package main
import "sync"
func main () {
    wg := sync.WaitGroup{}
    si := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    for i := range si {
        wg.Add(i)
        //這裡有一個實參到形參的值拷貝
        go func(a int) {
            println(a)
            wg.Done()
        }(i)
    }

    wg.Wait ()
}
執行結果:

9
0
1
2
3
4
5
6
7
8

可以看到新程式的執行結果符合預期,這個不能說是缺陷,而是Go語言設計者為了效能而選擇的一種設計方案,因為大多情況下 for 迴圈塊裡的程式碼是在同一個 goroutine 裡執行的,為了避免空間的浪費和 GC 的壓力,複用了 range 疊代臨時變數,語言使用者明白這個規約,在 for 迴圈下呼叫並行時要複製臨時變數後再使用,不要直接參照 for 疊代變數。