Go 1.22 中的 For 循环
原文在这里。
由 David Chase and Russ Cox 发布于2023年9月19日
Go 1.21 版本包含了对 for 循环作用域的预览更改,我们计划在 Go 1.22 中发布此更改,以消除其中一种最常见的 Go 错误。
问题
如果你写过一定量的 Go 代码,你可能犯过一个错误,即在迭代结束后仍然保留对循环变量的引用,此时它会取一个你不希望的新值。例如,思考下面的程序:
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
这三个创建的 goroutine 都在打印同一个变量 v,所以它们通常会打印出 "c"、"c"、"c",而不是以某种顺序打印出 "a"、"b" 和 "c"。
Go FAQ 中的条目 "What happens with closures running as goroutines?" 给出了这个例子,并指出 "在使用闭包与并发时可能会引起一些困惑"。
尽管上面的问题通常都涉及并发,但也不全是。这个例子虽然没有使用 goroutine,但仍然存在相同的问题:
func main() {
var prints []func()
for i := 1; i <= 3; i++ {
prints = append(prints, func() { fmt.Println(i) })
}
for _, print := range prints {
print()
}
}
这种错误已经在许多公司中引发了生产问题,包括 Lets Encrypt 中的一个公开记录的问题。在那个实例中,循环变量的意外捕获分散在多个函数中,更难以注意到:
// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a
// protobuf authorizations map
func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) {
resp := &sapb.Authorizations{}
for k, v := range m {
// Make a copy of k because it will be reassigned with each loop.
kCopy := k
authzPB, err := modelToAuthzPB(&v)
if err != nil {
return nil, err
}
resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{
Domain: &kCopy,
Authz: authzPB,
})
}
return resp, nil
}
这段代码的作者显然对这个问题有所了解,因为他们复制了 k
。但是,事实证明,在构建其结果时,modelToAuthzPB
使用了 v
中字段的指针,所以循环还需要复制 v
。
尽管我们已经编写了一些工具来识别这些错误,但是很难分析变量的引用是否超出了其迭代的范围。这些工具必须在误报和漏报之间做出选择。go vet
和 gopls
使用的 loopclosure
分析器选择了漏报,只有在确定存在问题时才会报告,但会错过其他情况。其他检查器则选择了误报,将正确的代码误认为是错误的。我们对添加了 x := x
行的开源 Go 代码进行了分析,期望找到 bug 修复。然而,我们发现许多不必要的行被添加进去,这表明尽管流行的检查器存在相当高的误报率,但开发人员仍然添加这些行来满足检查器的要求。
我们发现的一对示例特别有启发性:
在某个程序中,出现了以下差异:
for _, informer := range c.informerMap {
+ informer := informer
go informer.Run(stopCh)
}
在另一个程序中:
for _, a := range alarms {
+ a := a
go a.Monitor(b)
}
这两个差异中,一个是 bug 修复,另一个是不必要的更改。除非你对涉及的类型和函数有更多了解,否则无法确定哪个是哪个。
修复
在 Go 1.22 中,我们计划更改 for 循环,使这些变量具有每次迭代的作用域,而不是每次循环的作用域。这个改变将修复上面的例子,使它们不再是有错误的 Go 程序;它将解决由这些错误引起的生产问题;并且它将消除需要不准确的工具来提示用户对其代码进行不必要更改的需求。
为了确保与现有代码的向后兼容性,新的语义将仅适用于在其 go.mod
文件中声明了 go 1.22
或更高版本的模块中的包。这个每个模块的决策为开发人员提供了对代码库中新语义逐步更新的控制。还可以使用 //go:build
行来控制每个文件的决策。
旧代码将继续与今天完全相同:修复仅适用于新的或已更新的代码。这将使开发人员能够控制特定包中语义何时发生变化。由于我们的向前兼容性工作,Go 1.21 将不会尝试编译声明了 go 1.22 或更高版本的代码。我们在 Go 1.20.8 和 Go 1.19.13 的点发布版本中包含了一个具有相同效果的特殊情况,因此当发布 Go 1.22 时,依赖于新语义的代码将永远不会使用旧语义进行编译,除非人们使用非常旧且不受支持的 Go 版本。
修复预览
Go 1.21 包含了作用域更改的预览版本。如果您在环境中设置了 GOEXPERIMENT=loopvar
并编译您的代码,那么新的语义将应用于所有循环(忽略 go.mod 中的 go 行)。例如,要检查在将新的循环语义应用于您的包及其所有依赖项后,您的测试是否仍然通过,您可以执行以下操作:
GOEXPERIMENT=loopvar go test
我们在 Google 内部的 Go 工具链中进行了补丁,从 2023 年 5 月初开始,在所有构建过程中强制启用了这种模式,并且在过去的四个月中,我们没有收到任何关于生产代码的问题报告。
您还可以尝试一些测试程序,通过在程序顶部包含一个 // GOEXPERIMENT=loopvar
注释来更好地理解循环语义,就像这个程序中一样。(此注释仅适用于 Go Playground。)
验证测试
尽管我们在生产环境中没有遇到问题,但为了做好准备,我们确实需要纠正许多有问题的测试,这些测试并没有测试它们认为的内容,就像这个例子一样:
func TestAllEvenBuggy(t *testing.T) {
testCases := []int{1, 2, 4, 6}
for _, v := range testCases {
t.Run("sub", func(t *testing.T) {
t.Parallel()
if v&1 != 0 {
t.Fatal("odd v", v)
}
})
}
}
在 Go 1.21 中,这个测试通过是因为 t.Parallel
阻塞了每个子测试,直到整个循环完成,然后并行运行所有子测试。当循环完成时,v
的值总是 6,而所有子测试都检查 6 是否为偶数,所以测试通过了。但实际上,这个测试应该失败,因为 1 不是偶数。修复 for 循环暴露了这种有问题的测试。
为了帮助准备这种发现,我们在 Go 1.21 中提高了 loopclosure
分析器的精确性,使其能够识别和报告这个问题。你可以在 Go Playground 上的这个程序中看到报告。如果 go vet
在你自己的测试中报告了这种问题,修复它们将更好地为 Go 1.22 做准备。
如果你遇到其他问题,FAQ中提供了示例和详细信息的链接,可以使用我们编写的工具来识别在应用新语义时导致测试失败的具体循环。
更多详情
要了解更多关于这个改变的信息,请参阅设计文档和常见问题解答(FAQ)。这些资源将提供更详细的解释和指导,帮助您更好地理解这个改变以及如何适应它。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
Go 1.22 中的 For 循环的更多相关文章
- TMsgThread, TCommThread -- 在delphi线程中实现消息循环(105篇博客,好多研究消息的文章)
在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使用很方便,但是有时候我们需要在线程类中使用消息循环,delphi没有提供. 花了两天的事件研究了 ...
- Java中的do-while循环——通过示例学习Java编程(11)
作者:CHAITANYA SINGH 来源:https://www.koofun.com/pro/kfpostsdetail?kfpostsid=22&cid=0 在上一篇教程中,我们讨论了w ...
- Fedora 22中的RPM软件包管理工具
Introduction The RPM Package Manager (RPM) is an open packaging system that runs on Fedora as well a ...
- Fedora 22中的用户和用户组管理
The control of users and groups is a core element of Fedora system administration. This chapter expl ...
- Fedora 22中的日期和时间配置
Introduction Modern operating systems distinguish between the following two types of clocks: A real- ...
- Oracle中三种循环(For、While、Loop)
1.ORACLE中的GOTO用法 DECLARE x number; BEGIN x := 9; <<repeat_loop>> --循环点 x := x - 1; DBMS_ ...
- cocos2dx常见的46中+22中动作详解
cocos2dx常见的46中+22中动作详解 分类: iOS2013-10-16 00:44 1429人阅读 评论(0) 收藏 举报 bool HelloWorld::init(){ ///// ...
- TMsgThread, TCommThread -- 在delphi线程中实现消息循环
http://delphi.cjcsoft.net//viewthread.php?tid=635 在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使 ...
- 深入了解JavaScript中的for循环
在ECMAScript5中,有三种for循环,分别是: 简单for循环 for-in forEach 在ES6中,新增了一种循环 for-of 简单for循环 const arr = [1, 2, 3 ...
- 【测试技术】ant中的for循环用法
有的时候,我们希望ant中也能类似脚本语言一样进行for循环,以实现一些重复性工作.由于ant核心包并未提供此功能,所以需要下载一个扩展包扔到ant的lib目录下去.详细步骤如下: 1.下载核心包:a ...
随机推荐
- 【Ubuntu22.04】配置静态IP地址和FTP服务
## 一.配置静态IP 1. 使用命令`ip a`查看当前网卡名称,Ubuntu22.04默认网卡为ens33: 类型.通道是一种特殊的数据结构,可以在协程之间进行传递数据,从而实现协程之间的通信和同步. ...
- MRS-MRS相同功能代码管理应用笔记
MRS相同功能代码管理应用笔记 使用 MounRiver(以下简称 MRS )进行 RISC-V 单片机开发时,工程目录下往往存在多个文件夹与文件,我们只需要着重关注截图中红框所示的部分,它们自上而下 ...
- 企业级logstash简单使用(ELK)
企业级logstash简单使用(ELK) 要使用logstash收集到Elasticsearch的方式,需确保logstash版本与es版本一致. 由于我也是刚刚研究使用,所以本文暂不会出现原理性的东 ...
- vue3 安装 3d-force-graph
1.首先创建vue3的项目 2.创建好后通过开发工具打开项目并打开命令行,输入指令 npm install 3d-force-graph 安装即可 3.在使用的页面中引入 3d-force-graph ...
- 国内可用的GPT4镜像站
我做了个镜像站,GPT4的费用目前太贵了. 虽然做了个低价的月费,但基本上亏本在做,接口的实际费用高出太多. 运行了1个月,每天有几十人在用吧. 有兴趣的可以收藏一下 GPT4的验明正身的问题&quo ...
- Windows系统使用Nginx部署Vue
Nginx是什么? Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 ,同时也提供了IMAP/POP3/SMTP服务.Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的R ...
- Ui2Code+ChatGPT助力低代码搭建
前言 低代码开发平台(LCDP),是低代码或无代码通过快速搭建配置的方式完成一个应用程序的开发与上线,可视化低代码就是可视化的DSL,它的优点更多的是来源可视化,相对的,它的局限性也还是来源于可视化, ...