最近在開發過程中遇到問題,追蹤了很久后發現是golang的經典問題,在for循環中使用了goroutine,在goroutine中使用了for循環的參數。
問題現象:
在使用rabbitmq進行數據傳遞時,發送端在一次循環中發送了8000條id不同的數據到rabbitmq的隊列中,接收端監聽該隊列並從rabbitmq中取數據。接收到的數據在程序中處理后寫入數據庫,結果發現數據中並沒有寫入8000條數據。最后定位原因為:在接收數據時在for循環中使用go協程,導致同時收到兩條數據時,協程都是使用的后一條數據,入庫因為是同一條數據,導致主鍵重復,插入失敗,所以數據庫中沒有8000條數據。錯誤代碼大致如下:
1
2
3
4
5
|
for d := range msgs {
go func() {
handler(d)
}()
}
|
用一個簡單的程序模擬該錯誤為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(
2 * time.Second)
}
|
輸出為:
7
10
10
10
10
10
10
10
10
10
問題解析:
閉包go協程里面引用的是變量i的地址;所有的go協程啟動后等待調用,在上面的協程中,部分協程很可能在for循環完成之后才被調用,所以輸出結果很多都是10;正常輸出最多到9哦
解決方法一
通過參數傳遞數據到協程
1
2
3
4
5
|
for i := 0; i < 10; i++ {
go func(data int) {
fmt.Println(data)
}(i)
}
|
解決方法二
在for循環中加一個臨時變量tmp,每次將i的值賦值給tmp,然后將tmp通過參數傳進協程。
此方法可以解決不能通過參數傳遞數據的情況(某些第三方庫不能傳參數)
1
2
3
4
5
6
|
for i := 0; i < 10; i++ {
tmp := i
go func(data int) {
fmt.Println(data)
}(tmp)
}
|
產生上述問題的本質是,golang的for循環會使用同一個變量來存儲迭代過程中的臨時變量,在將該變量傳遞給goroutine時,goroutine得到的是該變量的地址,又由於goroutine的啟動與調度機制有關,可能for循環執行完后,goroutine才開始調度,所以導致多個goroutine訪問的是同一個數據。