簡單聊聊Go for range中容易踩的坑
前言
為瞭讓大傢更好的理解本期知識點,先介紹以下幾個知識點:線性結構、非線性結構、循環、迭代、遍歷、遞歸。
線性結構:數組、隊列
非線性結構:樹、圖
循環(loop):最基礎的概念,所有重復的行為都是循環
遞歸(recursion):在函數內調用自身,將復雜情況逐步轉化成基本情況
(數學)迭代(iterate):在多次循環中逐步接近結果
(編程)迭代(iterate):按順序訪問線性結構中的每一項
遍歷(traversal):按規則訪問非線性結構中的每一項
下面會挑選幾個經典的案例,一塊來探討下,看看如何避免掉坑,多積累積累采坑經驗。
1. for+傳值
先來到開胃菜,熱熱身~
type student struct { name string age int } func main() { m := make(map[string]student) stus := []student{ {name: "張三", age: 18}, {name: "李四", age: 23}, {name: "王五", age: 26}, } for _, stu := range stus { m[stu.name] = stu } for k, v := range m { fmt.Println(k, "=>", v.name) } }
不出意料,輸出結果為:
李四 => 李四
王五 => 王五
張三 => 張三
這題比較簡單,就是簡單的傳值操作,大傢應該都能答上來。下面加大難度,改為傳址操作
2. for+傳址
將案例一改為傳址操作
type student struct { name string age int } func main() { m := make(map[string]*student) stus := []student{ {name: "張三", age: 18}, {name: "李四", age: 23}, {name: "王五", age: 26}, } for _, stu := range stus { m[stu.name] = &stu } for k, v := range m { fmt.Println(k, "=>", v.name) } }
好好想想應該輸出什麼結果呢?還是跟案例一是一樣的結果嗎?難道會有坑?
不出意料,還是出瞭意外,輸出結果為:
張三 => 王五
李四 => 王五
王五 => 王五
為什麼呢?
- 首先,關鍵點在於Go的for循環,對
循環變量stu
每次是循環並不是迭代(簡單的說,就是對循環變量stu
隻會做一次聲明和內存地址的分配,後面循環就是不斷更新值); - 所以,取址操作
&stu
,其實都是取的同一個變量的地址,隻是值被循環更新為最後一個元素的值; - 最終,輸出的
v.name
,都是最後一個元素的name為王五
。
解決方案:
在for循環中,做同名變量覆蓋stu:=stu
(即重新聲明一個局部變量,做值拷貝,避免相互影響)
type student struct { name string age int } func main() { m := make(map[string]*student) stus := []student{ {name: "張三", age: 18}, {name: "李四", age: 23}, {name: "王五", age: 26}, } for _, stu := range stus { stu := stu //同名變量覆蓋 m[stu.name] = &stu } for k, v := range m { fmt.Println(k, "=>", v.name) } }
輸出結果:
張三 => 張三
李四 => 李四
王五 => 王五
3.for+閉包
在for循環裡,做閉包操作,也是很容易掉坑的。看看下面輸出什麼?
var prints []func() for _, v := range []int{1, 2, 3} { prints = append(prints, func() { fmt.Println(v) }) } for _, print := range prints { print() }
一眼看過去,感覺是輸出1 2 3,但實際會輸出 3 3 3
為什麼呢?
- 首先,在分析瞭案例二後,我們知道瞭Go的for循環對
循環變量v
,其實每次是循環並不是迭代; - 然後,
閉包=函數+引用環境
,在同一個引用環境下,循環變量v的值會被不斷的覆蓋; - 所以最終,在打印時,輸出的v,都是最後一個值3。
解決方案:
和案例二解決方案一樣,是在for循環中,做同名變量覆蓋v:=v
var prints []func() for _, v := range []int{1, 2, 3} { v := v //同名變量覆蓋 prints = append(prints, func() { fmt.Println(v) }) } for _, print := range prints { print() }
輸出結果:
1
2
3
4. for+goroutine
在for循環裡,起goroutine協程,也是很迷惑很容易掉坑的。看看下面輸出什麼?
var wg sync.WaitGroup strs := []string{"1", "2", "3", "4", "5"} for _, str := range strs { wg.Add(1) go func() { defer wg.Done() fmt.Println(str) }() } wg.Wait()
一眼看過去,感覺是會無序輸出1 2 3 4 5,但實際會輸出 5 5 5 5 5
為什麼呢?
- 首先,要記得Go的for循環對
循環變量str
,其實每次是循環並不是迭代; - 然後,main協程會和新起的協程做相互博弈,看誰執行更快,按這個案例執行情況來看,main協程執行速度明顯比新起的協程會更快,所以str被更新為最後一個元素值5(備註:並非絕對);
- 最終,在新起的協程中,使用str時值都為5,作為結果去輸出;
- 拓展:如果在新起協程前,sleep個5s,輸出結果又會截然不同,感興趣的同學可以自行實驗下,然後逐步深入地瞭解下GMP調度機制。
解決方案:
和前面兩個案例解決方案一樣,是在for循環中,做同名變量覆蓋str:=str
var wg sync.WaitGroup strs := []string{"1", "2", "3", "4", "5"} for _, str := range strs { str := str //同名變量覆蓋 wg.Add(1) go func() { defer wg.Done() fmt.Println(str) }() } wg.Wait()
輸出結果:
5
4
2
1
3
註意是1~5無序輸出
總結
for循環中做傳址、閉包、goroutine相關操作,千萬要註意,一不小心就會很容易掉坑。
使用好同名變量覆蓋v:=v
,這個解決大法,能很便捷的解決這一類問題。
到此這篇關於簡單聊聊Go for range中容易踩的坑的文章就介紹到這瞭,更多相關Go for range內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- golang基礎之waitgroup用法以及使用要點
- Go語言如何輕松編寫高效可靠的並發程序
- 在golang中使用Sync.WaitGroup解決等待的問題
- 詳解Go語言中Goroutine退出機制的原理及使用
- Go語言同步等待組sync.WaitGroup結構體對象方法詳解