前言
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()}输入后果:123
案例四:for+goroutine
在for循环里,起goroutine协程,也是很蛊惑很容易掉坑的。看看上面输入什么?
var wg sync.WaitGroupstrs := []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.WaitGroupstrs := []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()输入后果:54213留神是1~5无序输入
总结
for循环中做传址、闭包、goroutine相干操作,千万要留神,一不小心就会很容易掉坑。
应用好同名变量笼罩v:=v
,这个解决大法,能很便捷的解决这一类问题。
如果本文对你有帮忙,欢送点赞珍藏加关注,如果本文有谬误的中央,欢送指出!
本文参加了SegmentFault 思否写作挑战赛,欢送正在浏览的你也退出。