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)
  }
}

好好想想應該輸出什麼結果呢?還是跟案例一是一樣的結果嗎?難道會有坑?

不出意料,還是出了意外,輸出結果爲

張三 => 王五
李四 => 王五
王五 => 王五

爲什麼呢?

解決方案

在 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

爲什麼呢?

解決方案

和案例二解決方案一樣,是在 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

爲什麼呢?

解決方案

和前面兩個案例解決方案一樣,是在 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