脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|shell|

服务器之家 - 脚本之家 - Golang - 简单聊聊Go for range中容易踩的坑

简单聊聊Go for range中容易踩的坑

2023-03-21 14:04劲仔Go&王中阳Go Golang

for循环问题,在面试中经常都会被问到,并且在实际业务项目中也经常用到for循环,要是没用好,一不下心就掉坑,本文就来讲讲Go for range中容易踩的坑吧

前言

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

线性结构:数组、队列

非线性结构:树、图

循环(loop):最基础的概念,所有重复的行为都是循环

递归(recursion):在函数内调用自身,将复杂情况逐步转化成基本情况

(数学)迭代(iterate):在多次循环中逐步接近结果

(编程)迭代(iterate):按顺序访问线性结构中的每一项

遍历(traversal):按规则访问非线性结构中的每一项

下面会挑选几个经典的案例,一块来探讨下,看看如何避免掉坑,多积累积累采坑经验。

1. for+传值

先来到开胃菜,热热身~

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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+传址

将案例一改为传址操作

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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(即重新声明一个局部变量,做值拷贝,避免相互影响)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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循环里,做闭包操作,也是很容易掉坑的。看看下面输出什么?

?
1
2
3
4
5
6
7
var prints []func()
for _, v := range []int{123} {
  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

?
1
2
3
4
5
6
7
8
var prints []func()
for _, v := range []int{123} {
  v := v //同名变量覆盖  
  prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
  print()
}

输出结果:

1
2
3

4. for+goroutine

在for循环里,起goroutine协程,也是很迷惑很容易掉坑的。看看下面输出什么?

?
1
2
3
4
5
6
7
8
9
10
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

?
1
2
3
4
5
6
7
8
9
10
11
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 for range中容易踩的坑的文章就介绍到这了,更多相关Go for range内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://mp.weixin.qq.com/s/jdKIuPmb-Y0TGROdX97AQA

延伸 · 阅读

精彩推荐
  • GolangGo单体服务开发最佳实践总结

    Go单体服务开发最佳实践总结

    这篇文章主要介绍了Go单体服务开发最佳实践,通过本文详细跟大家分享一下如何使用 go-zero 快速开发一个有多个模块的单体服务,需要的朋友可以参考下...

    万俊峰Kevin7522022-09-29
  • Golang关于Go 空结构体的 3 种使用场景

    关于Go 空结构体的 3 种使用场景

    在今天这篇文章要给大家介绍得是Go 语言中几种常见类型的宽度,并且基于开头的问题 ”空结构体“ 进行了剖析,需要的朋友可以参考一下,希望对你有所...

    煎鱼eddycjy11852021-11-22
  • GolangGo语言利用Unmarshal解析json字符串的实现

    Go语言利用Unmarshal解析json字符串的实现

    本文主要介绍了Go语言利用Unmarshal解析json字符串的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    hp_cpp4812022-10-08
  • GolangGo 语言的结构体与方法

    Go 语言的结构体与方法

    结构体是 go 语言中一个比较重要的概念,在 c 语言中也有类似的东西。由于他们没有类的概念,结构体可以简单理解成类,是一个不同类型的数据构成的一...

    自然醒的笔记本4612021-04-21
  • GolangGoLang读取文件的10种方法实例

    GoLang读取文件的10种方法实例

    读取文件是所有编程语言中最常见的操作之一,下面这篇文章主要给大家介绍了关于GoLang读取文件的10种方法,文中通过实例代码介绍的非常详细,需要的朋友...

    GoGo在努力5252022-10-26
  • Golanggolang 监听服务的信号,实现平滑启动,linux信号说明详解

    golang 监听服务的信号,实现平滑启动,linux信号说明详解

    这篇文章主要介绍了golang 监听服务的信号,实现平滑启动,linux信号说明详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    胖达喵12072021-06-23
  • Golang在Golang中使用C语言代码实例

    在Golang中使用C语言代码实例

    这篇文章主要介绍了在Golang中使用C语言代码实例,本文先是给出了一个Hello World例子、Golang 引用 C例子,并总结了一些要注意的地方,需要的朋友可以参考下...

    Golang教程网3262019-11-17
  • GolangGoFrame框架数据校验之校验结果Error接口对象

    GoFrame框架数据校验之校验结果Error接口对象

    这篇文章主要为大家介绍了GoFrame框架数据校验之校验结果Error接口对象示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早...

    王中阳Go10452022-10-24