在 Go 語言中,當在循環中啟動協程(goroutine)時,如果在協程閉包中直接引用循環變量,可能會遇到一個常見的陷阱 - ??循環變量捕獲問題??。讓我詳細解釋一下:
問題背景
看這個代碼片段:
for i := 0; i < 10; i++ {go func() {fmt.Printf("i = %d\n", i) // 這里直接引用循環變量i}()
}
這段代碼會輸出什么?你可能會期待輸出 0 到 9 的數字,但實際上很可能輸出的是:
i = 10
i = 10
i = 10
...
問題原因
-
??閉包共享變量??:
- 所有協程共享同一個
i
變量(不是每個協程有自己的副本) - 當協程開始執行時,
i
的值可能已經是循環結束后的值
- 所有協程共享同一個
-
??執行時機??:
- 協程的啟動是異步的,不保證立即執行
- 循環執行非常快,可能在所有協程啟動前就已經結束
- 當協程實際執行時,
i
已經遞增到結束值(10)
-
??內存位置??:
- 所有協程都訪問同一個內存地址(變量
i
) - 不會為每次迭代創建新變量
- 所有協程都訪問同一個內存地址(變量
解決方案:將循環變量作為參數傳遞
for i := 0; i < 10; i++ {go func(id int) { // 使用參數fmt.Printf("id = %d\n", id) // 使用參數值}(i) // 將當前i的值作為參數傳入
}
這種寫法解決了問題:
-
??按值傳遞??:
i
的當前值被復制到參數id
中- 每個協程得到自己的
id
副本
-
??獨立變量??:
- 每個協程有自己的
id
變量(不是共享同一個) - 協程執行時,
id
值已經固定(不會受后續循環影響)
- 每個協程有自己的
-
??安全性??:
- 即使循環繼續執行,已啟動協程的參數值不會改變
- 解決了變量捕獲的競態問題
深入技術解釋
在 Go 中:
- 循環變量
i
在每次迭代中重復使用(不是新創建) - 協程閉包捕獲的是變量(不是值),所以共享同一個變量
- 通過參數傳遞,實際上是值傳遞,創建了獨立的變量副本
- 這是 Go 語言中處理循環中啟動協程的標準模式
在你的計數器代碼中的應用
在你的代碼中:
go func(id int) { // 使用參數id接收defer wg.Done()for j := 0; j < 1000; j++ {mu.Lock()counter++mu.Unlock()}fmt.Printf("協程 %d 完成1000次遞增\n", id)
}(i) // 傳遞當前的i值
- 確保每個協程打印正確的ID(0-9)
- 避免所有協程都打印相同ID的混淆
- 解決了潛在的數據競爭問題
其他解決方案
另一種解決方式是:
for i := 0; i < 10; i++ {i := i // 創建局部副本go func() {fmt.Printf("i = %d\n", i) // 使用局部副本}()
}
但這不如作為參數傳遞直觀明確,且增加了一行代碼。
總結
在循環中啟動協程時,??始終將循環變量作為參數傳遞給協程函數??是:
- 安全可靠的編碼習慣
- 避免閉包捕獲陷阱的最佳實踐
- Go 并發編程中的重要技巧
這個模式確保每個協程獲得正確的變量值,避免了微妙的并發錯誤,是Go語言中處理循環和并發結合的標準方法。