Go for range 一不下心就掉坑
前言
for 循環問題,在面試中經常都會被問到,並且在實際業務項目中也經常用到 for 循環,要是沒用好,一不下心就掉坑。
下面會挑選幾個經典的案例,一塊來探討下,看看如何避免掉坑,多積累積累採坑經驗。
案例一: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)
}
}
不出意料,輸出結果爲:
李四 => 李四
王五 => 王五
張三 => 張三
這題比較簡單,就是簡單的傳值操作,大家應該都能答上來。下面加大難度,改爲傳址操作
案例二: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)
}
}
輸出結果:
張三 => 張三
李四 => 李四
王五 => 王五
案例三: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
案例四: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 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/C-mPtJZkDgC6e2nFtCJbtA