大多数Go程序员都走过的坑盘点解析

 更新时间:2023年12月20日 08:57:06   作者:晁岳攀(鸟窝) 鸟窝聊技术  
这篇文章主要为大家介绍了大多数Go程序员都走过的坑盘点解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

脚本之家 / 编程助手:解决程序员“几乎”所有问题!
脚本之家官方知识库 → 点击立即使用

循环变量

说起每个程序员必犯的错误,那还得是"循环变量"这个错误了,就连 Go 的开发者都犯过这个错误,这个错误在 Go 的 FAQ 中也有提到

What happens with closures running as goroutines?[1]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
    var wg sync.WaitGroup
 
    values := []string{"a", "b", "c"}
    for _, v := range values {
        wg.Add(1)
        go func() {
            fmt.Println(v)
            wg.Done()
        }()
    }
 
    wg.Wait()
}

你可能期望能输出abc这三个字符(可能顺序不同),但是实际可能输出的是ccc。这是因为循环变量的作用域是整个循环,而不是单次迭代,所以在循环体中使用的变量是同一个变量,而不是每次迭代都是一个新的变量。

这个错误有时候隐藏很深,即使没有 goroutine,也有可能,比如下面的代码,并没有使用额外的 goroutine 和闭包,也是有问题的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
 "fmt"
)
type Char struct {
 Char *string
}
func main() {
 var chars []Char
 values := []string{"a", "b", "c"}
 for _, v := range values {
  chars = append(chars, Char{Char: &v})
 }
 for _, v := range chars {
  fmt.Println(*v.Char)
 }
}

输出也大概率是ccc,因为给每个Char的字段赋值的是 v 的指针,v 在整个循环中都是一个变量,所以最后的结果都是c

Go 团队很早也意识到这个问题了,但是考虑到兼容的问题,大家的容忍程度,那就这样了。每个 Go 程序员都在这里摔一跤,也就长记性了,所以一直没有改变这个设计。我在这里摔了好多跤,以至于我写 for 循环的时候都战战兢兢的,和 Russ Cox 统计的网上的处理一样,不管有无必要,很多时候我都是先把循环变量赋值给一个局部变量,然后再使用,比如下面的代码:

1
2
3
4
5
6
7
8
for _, v := range values {
    v := v
    wg.Add(1)
    go func() {
        fmt.Println(v)
        wg.Done()
    }()
}

变量只在循环体中使用

今年 5 月份的时候,Russ Cox 忍不住了,提了一个提案#60078[2],提案的内容是在 for 循环中,如果变量只在循环体中使用,那么就会在每次迭代中创建一个新的变量,而不是使用同一个变量。这个提案引起了很多人的关注,很多人都在讨论这个提案,这个提案被接收了,具体提案内容在文档中Proposal: Less Error-Prone Loop Variable Scoping[3]。

如果你使用 Go 1.21, 你可以开始这个功能,使用GOEXPERIMENT=loopvar go run main.go运行上面的程序,会输出cba这样的输出,不再是ccc了。这个特性在 Go 1.22 中会默认开启,不需要设置GOEXPERIMENT了。还有一两个月才能正式发布 go 1.22,

大家可以使用 gotip 测试:

1
2
3
4
$ gotip run main.go
a
b
c

不只是for-range,下面的3-clause也是同样的问题:

1
2
3
4
5
6
7
8
9
10
func main() {
 var ids []*int
 for i := 0; i < 3; i++ {
  i = 10
 }
 
 for _, id := range ids {
  fmt.Println(*id)
 }
}

Go 1.22 中也会修复这个问题。

对比C#语言

C#语言就只修改了for-range语句,3-clause语句就没有修改, Go 两种都做了修改。

但是, 问题就来了哈,像下面的代码,Go 1.22 和以前的代码会一样么?

1
2
3
4
5
6
7
8
9
10
11
func main() {
 var ids []*int
 for i := 0; i < 3; i++ {
        i = 10
  ids = append(ids, &i)
 }
 
 for _, id := range ids {
  fmt.Println(*id)
 }
}

如果用 Go 1.21,它会输出11。如果用 Go 1.22,它会输出10。原因还是在于这个提案实现后,每次迭代的时候,都会创建一个新的变量,所以ids中的元素都是指向不同的变量,而不是同一个变量。

看起来打破了向下兼容的承诺,你如果先前就想利用这个 corner case 的话,Go1.22 已经不兼容了。

更进一步,你会发现再执行3-clause的第三条 clause 的时候,变量已经被重新创建,比如下面的代码:

1
2
3
4
5
6
7
func main() {
 for i, p := 0, (*int)(nil); i < 3; println("3rd-clause:", &i, p) {
  p = &i
  fmt.Println("loop body:", &i, p)
  i++
 }
}

输出:

$gotip run main.go
loop body: 0x14000120018 0x14000120018
3rd-clause: 0x14000120030 0x14000120018 // &i已经变为0x14000120030
loop body: 0x14000120030 0x14000120030
3rd-clause: 0x14000120038 0x14000120030 // &i已经变为0x14000120038
loop body: 0x14000120038 0x14000120038
3rd-clause: 0x14000120040 0x14000120038 // &i已经变为0x14000120040

参考资料

https://github.com/golang/go/issues/60078

https://go.dev/doc/faq#closures_and_goroutines 

以上就是大多数Go程序员都走过的坑盘点解析的详细内容,更多关于Go程序员都走过的坑的资料请关注脚本之家其它相关文章!

蓄力AI

微信公众号搜索 “ 脚本之家 ” ,选择关注

程序猿的那些事、送书等活动等着你

原文链接:https://mp.weixin.qq.com/s/ReAUdcByet0_hfKYGEKUwA

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 reterry123@163.com 进行投诉反馈,一经查实,立即处理!

相关文章

  • Go语言中的各类运算操作符详解

    Go语言中的各类运算操作符详解

    本文全面探讨了Go语言中的各类运算操作符,从基础的数学和位运算到逻辑和特殊运算符,文章旨在深入解析每一种运算操作符的工作原理、应用场景和注意事项,以帮助开发者编写更高效、健壮和可读的Go代码,</P><P>
    2023-09-09
  • Golang 断言与闭包使用解析

    Golang 断言与闭包使用解析

    这篇文章主要介绍了Golang 断言与闭包使用解析,Go中的断言用于判断变量的类型,更多相关内容需要的朋友可以参考一下
    2022-07-07
  • golang 切片截取参数方法详解

    golang 切片截取参数方法详解

    这篇文章主要介绍了golang 切片截取参数方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Go语言学习之映射(map)的用法详解

    Go语言学习之映射(map)的用法详解

    Map是一种无序的键值对的集合。这篇文章主要为大家详细介绍了Go语言中映射的用法,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的可以参考一下
    2022-04-04
  • Go程序的init函数在什么时候执行

    Go程序的init函数在什么时候执行

    在Go语言中,init 函数是一个特殊的函数,它用于执行程序的初始化任务,本文主要介绍了Go程序的init函数在什么时候执行,感兴趣的可以了解一下
    2023-10-10
  • 使用Go语言实现Yaml编码和解码的方法详解

    使用Go语言实现Yaml编码和解码的方法详解

    在这篇文章中,我们将介绍如何使用Go语言编写代码来实现Yaml编码和解码,文中有详细的代码示例供大家参考,对大家的学习和工作有一定的帮助,需要的朋友可以参考下
    2023-11-11
  • golang:json 反序列化的[]和nil操作

    golang:json 反序列化的[]和nil操作

    这篇文章主要介绍了golang:json 反序列化的[]和nil操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go中并发控制的实现方式总结

    Go中并发控制的实现方式总结

    在Go实际开发中,并发安全是老生常谈的事情,在并发下,goroutine之间的存在数据资源等方面的竞争,为了保证数据一致性、防止死锁等问题的出现,在并发中需要使用一些方式来实现并发控制,本文给大家总结了几种实现方式,需要的朋友可以参考下
    2023-12-12
  • Go处理PDF的实现代码

    Go处理PDF的实现代码

    这篇文章主要介绍了Go处理PDF的实现代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • Go语言MySQLCURD数据库操作示例详解

    Go语言MySQLCURD数据库操作示例详解

    这篇文章主要为大家介绍了Go语言MySQLCURD数据库操作示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12

最新评论