首页 > 编程语言 >每个Go程序员必犯之错之切片循环错误

每个Go程序员必犯之错之切片循环错误

时间:2024-01-18 09:01:59浏览次数:25  
标签:必犯 变量 ids go range Go 之错 main

每个Go程序员必犯之错

原创 晁岳攀(鸟窝) 鸟窝聊技术 2023-12-18 08:48 发表于北京 听全文

说起每个程序员必犯的错误,那还得是"循环变量"这个错误了,就连 Go 的开发者都犯过这个错误,这个错误在 Go 的 FAQ 中也有提到What happens with closures running as goroutines?[1]:

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 和闭包,也是有问题的:


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 统计的网上的处理一样,不管有无必要,很多时候我都是先把循环变量赋值给一个局部变量,然后再使用,比如下面的代码:

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 测试:

$ gotip run main.go
a
b
c

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

func main() {
 var ids []*int
 for i := 0; i < 3; i++ {
  i = 10
 }

 for _, id := range ids {
  fmt.Println(*id)
 }
}

Go 1.22 中也会修复这个问题。C#语言就只修改了for-range语句,3-clause语句就没有修改, Go 两种都做了修改。

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

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 的时候,变量已经被重新创建,比如下面的代码:

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

参考资料

[1]

What happens with closures running as goroutines?: https://go.dev/doc/faq#closures_and_goroutines

[2]

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

[3]

Proposal: Less Error-Prone Loop Variable Scoping: https://go.googlesource.com/proposal/+/master/design/60078-loopvar.md

 

Go86 go87 golang86 Golang81 Go · 目录 上一篇盘点2023年在地球上举行的Gopher盛会下一篇鲜有人了解的同步原语Phaser,和Barrier有啥区别? 阅读原文 阅读 2440 鸟窝聊技术 ​ 分享此内容的人还喜欢   Go运行时的并发原语     鸟窝聊技术 不看的原因   Go 语言为什么很少使用数组?     我关注的号 Golang语言开发栈 不看的原因   CentOS7.9下离线安装OctoMation编排自动化SOAR开源社区免费版     WalkingCloud 不看的原因   关注公众号后可以给作者发消息              

人划线

 

标签:必犯,变量,ids,go,range,Go,之错,main
From: https://www.cnblogs.com/cheyunhua/p/17971713

相关文章

  • 初中英语优秀范文100篇-061Reading Is a Good Habit-阅读是一种良好的习惯
    PDF格式公众号回复关键字:SHCZFW061记忆树1Agoodhabitcangiveusbenefitsallthelife.翻译养成良好习惯可以使我们终生受益简化记忆受益句子结构主语:"Agoodhabit"-主语是一个名词短语,表示一个良好的习惯。谓语动词:"cangive"-谓语动词是"cangive......
  • F - Hop Sugoroku
    F-HopSugorokuProblemStatementThereisarowof$N$squareslabeled$1,2,\dots,N$andasequence$A=(A_1,A_2,\dots,A_N)$oflength$N$.Initially,square$1$ispaintedblack,theother$N-1$squaresarepaintedwhite,andapieceisplacedonsquar......
  • 海外视频直播APP/多语言语聊APP提交Google Play,Easy Done详细步骤
    当APP开发完成后,最重要的一个环节是需要将APP提交到GooglePlay,山东布谷科技的技术人员根据以往操作经验,来基础介绍下提交流程。首先是GooglePlay的上架前准备:  创建开发者账号:首先,您需要拥有一个Google开发者账号。如果没有账号,您需要前往GooglePlay开发者控制台,注册并购......
  • 配置cargo国内源
    https://mirrors.tuna.tsinghua.edu.cn/help/crates.io-index.git/编辑$CARGO_HOME/config文件,添加以下内容:[source.crates-io]replace-with='mirror'[source.mirror]registry="https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"注:$C......
  • Golang nil != nil
    先从一段代码看起,下面这个代码是将传入的对象转换成JSONstring并返回回去,其中,如果判断i==nil时,会返回""。funcToJSONString(iinterface{})string{ifi==nil{return""}bytes,_:=json.Marshal(i)returnstring(bytes)}这段代码初看并没有......
  • 为什么要避免在 Go 中使用 io.ReadAll
    ioutil包在go1.16版本已弃用。io.ReadAll()实现://src/io/io.gofuncReadAll(rReader)([]byte,error){//创建一个512字节的buf b:=make([]byte,0,512) for{ iflen(b)==cap(b){ //如果buf满了,则追加一个元素,使其重新分配内存 b=append(b,0)[......
  • golang 处理未确定json字符串
    json字符串转golangmap我们知道golang处理json字符串时,需要先转成struct,并且struct必须是确定的。有时候我们传递的json是不固定的,针对每种情况都写一个struct比较麻烦,有没有处理不确定json的方法呢?答案是有的 varastring="{\"a\":1,\"b\":\"xx\"}" varbmap[string]in......
  • `cargo build`报错:`failed to run custom build command for libgit2-sys v0.13.2+1.4
    cargobuild报错:failedtoruncustombuildcommandforlibgit2-sysv0.13.2+1.4.21问题背景在使用cargo编译cargo-cache时出现报错:Thefollowingwarningswereemittedduringcompilation:warning:[email protected]+1.4.2:Infileincludedfromlibgit2/src/pack.......
  • Go gin框架使用 SSEVENT
    我知道的是,是一个http长连接,有着类websocket的api;后端示例代码:packagemainimport( "fmt" "net/http" "time" "github.com/gin-gonic/gin")funcmain(){ router:=gin.Default() r.GET("/events",func(c*gin.Context......
  • 使用Go语言编写HTTP代理服务器
    在Go语言中,编写一个HTTP代理服务器相对简单且直观。代理服务器的主要职责是接收客户端的请求,然后将请求转发到目标服务器,再将目标服务器的响应返回给客户端。下面是一个简单的示例,展示如何使用Go语言编写一个基本的HTTP代理服务器:go复制代码package mainimport ("io" "log" "......