乐趣区

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

如果本文对你有帮忙,欢送点赞珍藏加关注,如果本文有谬误的中央,欢送指出!

本文参加了 SegmentFault 思否写作挑战赛,欢送正在浏览的你也退出。

退出移动版