开启成长之旅!这是我参加「日新方案 2 月更文挑战」的第 8天,点击查看活动详情

前言

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,这个解决大法,能很快捷的解决这一类问题。

如果本文对你有协助,欢迎点赞保藏加关注,如果本文有过错的当地,欢迎指出!