草庐IT

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

劲仔Go&王中阳Go 2023-03-28 原文

前言

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

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

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

有关Go for range 一不小心就掉坑里了的更多相关文章

  1. javascript - react-hot-loader 的 webpack 输出文件去哪里了? - 2

    首先让我说我设置的一切都有效,这只是一个困扰我的问题,我很想得到答案。我正在使用react-hot-boilerplate项目(https://github.com/gaearon/react-hot-boilerplate)。然而,在webpack.config.js中,这个设置让我困惑不已:output:{path:path.join(__dirname,'dist'),filename:'bundle.js',publicPath:'/static/'},在此配置中,输出文件似乎应该进入项目根目录中的dist文件夹。即使我手动创建dist文件夹(我知道我不应该这样做),也不会输出

  2. javascript - 编写 express.js 应用程序。辅助方法去哪里了? - 2

    所以我开始使用express.js——我的第一个JS网络开发框架。我没有做任何小事,而是开始了一个更大的项目。我在学习,同时也在build。来自Python/Flask背景,express似乎非常复杂。就像在python中一样,如果我想要一个辅助方法,我可以将它放在文件顶部或新模块中,然后导入它。super简单。但是在node/express中,事情是异步的,一切都在exports或module.exports中(??)。辅助方法去哪里了?我如何通过回调调用它们?在anotherquestion我问,我多次进行相同类型的计算。在Python中,我会编写一个方法(带有if语句和参数),并

  3. 搭建hadoop集群初次格式化namenode时不小心格式化了多次,主节点namenode或者从节点datanode进程不能启动,怎么办? - 2

    我们在搭建完hadoop集群时,初次启动HDFS集群,需要对主节点进行格式化操作,其本质是清理和做一些准备工作,因为此时的HDFS在物理上还是存在的。而且主节点格式化操作只能进行一次。那我们在格式化时,不小心格式化多次,就会导致主从节点之间互相不识别。然后导致启动hadoop集群时,主节点的namenode进程可能不会启动或者从节点的datanode可能不会启动。这里给出一种解决方法:我们在配置hadoop的配置文件core-site.xml时,其中有一组参数hadoop.tmp.dir,它的值指定的是配置hadoop的临时目录我们把tmp目录删除,再重新格式化即可。先进入/export/se

  4. c# - DbSet<>.Local 是否需要特别小心使用? - 2

    几天来,我一直在努力从存储库(DbContext)中检索我的实体。我正在尝试将所有实体保存在一个原子操作中。因此,不同的实体一起代表对我有值(value)的东西。如果所有实体都是“有效”的,那么我可以将它们全部保存到数据库中。实体“a”已存储在我的存储库中,需要检索以“验证”实体“b”。这就是问题所在。我的存储库依赖于DbSet与Linq2Sql一起工作的类(Include()导航属性,例如)。但是,DbSet不包含处于“已添加”状态的实体。所以我(据我所知)有两个选择:使用ChangeTracker查看哪些实体可用并根据其EntityState将它们查询到一个集合中.使用DbSet.

  5. javascript - 为什么 undefined 不小于 1? - 2

    所以在大多数情况下,我已经能够使用类似于这些行的东西,但是Javascript给了我这个奇怪的结果。如果我取了一些值,结果发现它是未定义的,与整数相比,它似乎既不小于也不大于任何数字。这是为什么?if(undefined=1)alert("yes");elsealert("no");//thisalwaysalertsnoJSFiddle 最佳答案 没有operator'JavaScript中的错误,就像您在其他类型语言中发现的那样。因此,JavaScript将不兼容的类型与运算符评估为false。

  6. 修改后的代码只进行了git add操作不小心给他恢复了怎么找回来 - 2

    一份干净的代码在main.js里加了一行console.log(666),并且只进行了gitadd然后不小心给他reset了!gitreset--hard哦豁,没了?别急一样可以恢复,我们先执行gitfsck--lost-found然后我们去项目的.git下找到这个目录.git\lost-found\other发现了很多乱码名字的文件并且没有后缀,你没有猜错,这些就是gitadd过得文件,我们手动改后缀是可以查看内容的(找到文件再改)我们看那个改变的main.js文件大小,在这里插入代码片是5k。那我们从目录里找到大小相似的几个,也就是说可能是这几个,我们怎么确认呢,1.我们可以gitshow

  7. php - 在 PHP 中写入 STDERR 的内容去哪里了? - 2

    据我了解,如果未设置error_log配置指令,则会将错误发送到SAPI错误记录器。例如,它是Apache中的错误日志或CLI中的stderr。因此,如果我从命令行运行脚本,则会直接显示错误(除非重定向)。如果我从cron运行脚本,错误将通过电子邮件发送给cron作业所有者。那么,如果我将电子邮件通过管道传输到脚本会怎样?我刚开始编写脚本来处理退回邮件。我读过如果脚本输出任何东西,它会反弹回发件人。我不确定这是否适用于所有输出或仅适用于STDOUT。还是CLI中的STDERR等于STDOUT?如果我将error_log配置指令设置到一个文件,我可以使用trigger_error和err

  8. php - MVC 中的服务去哪里了? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭3年前。Improvethisquestion我问过几个开发者,每次都得到不同的答案。假设我在MVC框架中工作,并且我有一个名为validator的类。假设这个对象有一堆方法可以用来告诉你电子邮件或电话号码是否有效,或者给定的值是否确实包含内容。假设我想让这个服务成为我正在创建的模型的属性。我可以简单地将它注入(inject)到我的模型类的构造方法中。但是,此服务在MVC中的什么位置适合?是模型吗?文件应该存放在哪里?与模型?在它自己

  9. php - 如何在用户不小心关闭浏览器时恢复旧 session ? - 2

    我打算用PHP创建一个在线考试系统。如果用户不小心关闭了窗口,我可以采取什么步骤来恢复旧session?假设他已经回答了50个问题中的49个,突然断电(并且没有UPS)或者他不小心关闭了窗口(即使是错误的,如果他在window.unload事件上的javascript提示上单击是)然后重新打开浏览器,一切都丢失了。我可以做些什么来防止这种情况发生?提前致谢:) 最佳答案 您需要做以下两件事之一:在用户机器上保留当前状态-这必须通过cookie来完成。将当前状态保存在服务器上。第二个选项可能更可靠,它确实需要您经常与服务器保持联系。它

  10. java - 如果不小心删除了.metadata,如何获取项目列表 - 2

    如果.metadata目录被删除,有没有办法恢复Eclipse项目列表? 最佳答案 重新创建工作区后,您可以按照以下步骤将项目重新添加到工作区:选择File::Import::Other::General::ExistingProjectsintoWorkspace浏览根目录(如果项目共享一个共同的父目录,如工作区目录,请选择它)选中所有要重新导入的项目,然后单击Finish 关于java-如果不小心删除了.metadata,如何获取项目列表,我们在StackOverflow上找到一个类

随机推荐