Go for range 一不小心就掉坑里了

开发 项目管理
for循环问题,在面试中经常都会被问到,并且在实际业务项目中也经常用到for循环,要是没用好,一不下心就掉坑。

前言

为了让大家更好的理解本期知识点,先介绍以下几个知识点:线性结构、非线性结构、循环、迭代、遍历、递归。

  • 线性结构:数组、队列
  • 非线性结构:树、图
  • 循环(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」,可以通过以下二维码关注。

转载本文请联系「 程序员升级打怪之旅」公众号。

责任编辑:武晓燕 来源: 程序员升职加薪之旅
相关推荐

2021-07-28 05:01:29

Lombok前端测试

2021-01-05 22:49:37

Python编程语言Java

2021-04-30 08:21:22

Linux管道设计

2020-10-26 08:56:32

技术总监程序员

2021-10-07 16:45:06

MySQL数据库

2018-01-18 22:26:30

2021-03-29 18:47:53

APP服务端通信安全数据安全

2021-01-30 09:50:54

MySQL密码服务器

2019-09-19 20:47:29

刷脸支付人脸识别人工智能

2020-10-28 15:07:01

Arthas

2020-09-29 07:44:20

跨域前后端分离插件

2021-01-08 09:36:23

程序员比特币黑客

2021-03-26 06:14:26

Hashcode项目排查

2020-04-30 09:45:41

安卓App小米

2022-09-27 18:19:32

Java数据结构

2020-11-24 11:30:51

SpringJava代码

2019-07-29 14:38:35

服务器开发工具

2016-10-17 19:14:28

2021-05-11 16:20:02

网站HTTPHTTPS

2023-11-17 18:17:33

微信支付V3版本
点赞
收藏

51CTO技术栈公众号