首頁 > 軟體

簡單聊聊Go for range中容易踩的坑

2023-03-21 06:00:49

前言

為了讓大家更好的理解本期知識點,先介紹以下幾個知識點:線性結構、非線性結構、迴圈、迭代、遍歷、遞迴。

線性結構:陣列、佇列

非線性結構:樹、圖

迴圈(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內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com