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

 更新时间:2023年03月20日 16:03:53   作者:劲仔Go&王中阳Go  
for循环问题,在面试中经常都会被问到,并且在实际业务项目中也经常用到for循环,要是没用好,一不下心就掉坑,本文就来讲讲Go for range中容易踩的坑吧

前言

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

线性结构:数组、队列

非线性结构:树、图

循环(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 for range中容易踩的坑的文章就介绍到这了,更多相关Go for range内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深入理解Golang channel的应用

    深入理解Golang channel的应用

    channel是用于 goroutine 之间的同步、通信的数据结构。它为程序员提供了更高一层次的抽象,封装了更多的功能,这样并发编程变得更加容易和安全。本文通过示例为大家详细介绍了channel的应用,需要的可以参考一下
    2022-10-10
  • golang 中的 nil的场景分析

    golang 中的 nil的场景分析

    这篇文章主要介绍了golang 中的 nil,本文通过多种场景分析给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • Golang 实现 Redis系列(六)如何实现 pipeline 模式的 redis 客户端

    Golang 实现 Redis系列(六)如何实现 pipeline 模式的 redis 客户端

    pipeline 模式的 redis 客户端需要有两个后台协程负责 tcp 通信,调用方通过 channel 向后台协程发送指令,并阻塞等待直到收到响应,本文是使用 golang 实现 redis 系列的第六篇, 将介绍如何实现一个 Pipeline 模式的 Redis 客户端。
    2021-07-07
  • golang字符串转Time类型问题

    golang字符串转Time类型问题

    本文主要介绍了golang字符串转Time类型问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Go语言学习之new函数的用法详解

    Go语言学习之new函数的用法详解

    这篇文章主要为大家详细介绍了Go语言中new()函数的相关知识以及具体用法,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解一下
    2023-05-05
  • Go接口的用法详解

    Go接口的用法详解

    本文主要介绍了Go接口的用法详解,包括定义接口、实现接口、使用接口、空接口等,通过接口,可以实现多态性,即一个对象可以实现多个接口,从而实现不同接口的行为,感兴趣的可以了解一下
    2023-11-11
  • 对Go语言中的context包源码分析

    对Go语言中的context包源码分析

    这篇文章主要对Go语言中的context包源码进行分析,context包析是1.15,context包定义了一个Context类型过这个Context接口类型, 就可以跨api边界/跨进程传递一些值,下面我们就来对context包源码进行分析,需要的小伙伴可以参考一下
    2022-02-02
  • Go语言leetcode题解953验证外星语词典示例详解

    Go语言leetcode题解953验证外星语词典示例详解

    这篇文章主要为大家介绍了Go语言leetcode题解953验证外星语词典示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • go语言遍历文件夹示例

    go语言遍历文件夹示例

    这篇文章主要介绍了go语言遍历文件夹示例,本文直接给出实现代码,需要的朋友可以参考下
    2015-03-03
  • 详解如何在Golang中实现CORS(跨域)

    详解如何在Golang中实现CORS(跨域)

    很多时候,需要允许Web应用程序在不同域之间(跨域)实现共享资源,本文将简介跨域、CORS的概念,以及如何在Golang中如何实现CORS,文中有详细的示例代码,需要的朋友可以参考下
    2023-10-10

最新评论