原來Golang的Foreach這么坑!
前言
用過golang的同學,相信對「for range」是再熟悉不過了,可以說在任何語言中,循環(huán)遍歷都是常用的再也不能常用的一種方式,不過最近發(fā)現(xiàn)了一個問題,其實挺坑的,今天總結(jié)一下,希望對您有用。
坑1
咱們廢話不用多說,直接看例子。
現(xiàn)象
dataFromDb := []int{1,2,3} //從數(shù)據(jù)庫取出來的數(shù)據(jù)
var finalData []*int //目標數(shù)據(jù)
for _,i := range dataFromDb{
finalData = append(finalData, &i)
}
for _, final := range finalData{
fmt.Println(*final)
}
上面的例子很簡單
- 從數(shù)據(jù)庫取出來數(shù)據(jù) 1,2,3,賦值給 dataFromDb。
- 循環(huán)遍歷dataFromDb賦值給最終的目標數(shù)據(jù) finalData。
- 循環(huán)輸出目標數(shù)據(jù)finalData。
直觀的感受,上面簡直是一段簡單的不能再簡單的代碼了,相信大家會脫口而出最后finalData的值是1,2,3,但是我們實際運行一下,結(jié)果輸出的卻是
~/Sites/test ? go run main.go
3
3
3
結(jié)果輸出的全部都是3,顯然這與我們的認知是不符合的,但是為什么會這樣呢?如果想弄清這個原理,首先我們得知道for range到底干了什么。
for range原理
要想了解一個函數(shù)的原理,最好的方式就是看源碼,我們來看一看for range到底干了什么。
源碼來自于 go 編譯器的 「gc.walkrange」, 編譯器對 for range 表達式的解析如下:
// a為原始slice
ha := a
hv1 := 0
// slice長度
hn := len(a)
v1 := 0
v2 := nil // for i,v := range 中的 v
for ; h1 < hn ; h1++ {
tmp := ha[hv1]
v1,v2 := hv1,tmp
}
- 每一次for range,其實是先復制出來了一個副本ha,本質(zhì)上循環(huán)的其實是副本。
- for range中,go語言會額外創(chuàng)建一個新的 v2 變量存儲切片中的元素,「循環(huán)中使用的這個變量 v2 會在每一次迭代被重新賦值而覆蓋,賦值時也會觸發(fā)拷貝, 且循環(huán)中每次都使用的v2變量」。
回到問題
for _,i := range dataFromDb{
finalData = append(finalData, &i)
}
對于i來說,相當于 var i int,然后在循環(huán)的過程中 i=1,i=2,i=3 &i是指向i的地址,「所以&i是永遠不會變的」。
- 第一次循環(huán) &i指向i,i的值是1。
- 第二次循環(huán) &i指向i,i的值變成2了,同時也把第一次循環(huán)的i的結(jié)果改成2了。
- 第三次循環(huán) &i指向i,i的值變成3了,同時也把前兩次循環(huán)的i的結(jié)果改成3了。
如何解決
其實解決辦法很簡單,引入「中間變量」即可,代碼改成下面這個樣子。
dataFromDb := []int{1,2,3}
var finalData []*int
for _,i := range dataFromDb{
temp := i //引入中間變量,每一次循環(huán)都重新開辟了一個temp的空間
finalData = append(finalData, &temp)
}
for _, final := range finalData{
fmt.Println(*final)
}
代碼加入了「中間變量temp」temp:=i等價于。
var temp int
temp = 1
- 第一次循環(huán) temp開辟了一塊空間,指向了i,temp的值為1。
- 第二次循環(huán) temp「重新開辟了一塊空間」,指向了i,temp的值為2,因為是重新開辟的空間,所以不會影響到上一次循環(huán)。
- 第三次循環(huán) 原理同上一步。
坑2
現(xiàn)象
s := []int{1, 2, 3}
for _, v := range s {
go func() {
fmt.Println(v) // 輸出結(jié)果3 3 3
}()
}
select {}
大家可以想一想上面這段代碼會輸出什么
3
3
3
輸出結(jié)果居然全部都是最后一個值,這是為什么呢?
原因
在沒有將變量 v 的拷貝值傳進匿名函數(shù)之前,只能獲取最后一次循環(huán)的值,這是新手最容易遇到的坑。
解決辦法
解決辦法其實比較簡單,在閉包函數(shù)上增加參數(shù),并且與go rountine綁定即可。
s := []int{1, 2, 3}
for _, v := range s {
go func(v int) {
fmt.Println(v) // 輸出結(jié)果3 1 2
}(v)
}
select {}