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 ...
随机推荐
- Python获取系统当前时间并进行类型转换
# 1.使用Python获取系统当前时间print('---------下面是方法1-----------')import timeprint(time.strftime('%Y-%m-%d %H:% ...
- 前端学习C语言 - 开篇
前端学习C语言 - 开篇 前端学习C语言有很多理由:工作.兴趣或其他. C 语言几个常见的使用场景: 操作系统开发:Linux 操作系统的内核就是主要由 C 语言编写的.其他操作系统也广泛使用 C 语 ...
- Docker和Kubernetes与容器自动化扩展:最佳实践
目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.3 相关技术比较 3. 实现步骤与流程 3.1 准备工作:环境配置与依赖安装 3.2 核心模块实现 3.3 集成 ...
- C++面试八股文:std::vector和std::list,如何选择?
某日二师兄参加XXX科技公司的C++工程师开发岗位第24面: 面试官:list用过吗? 二师兄:嗯,用过. 面试官:请讲一下list的实现原理. 二师兄:std::list被称为双向链表,和C中手写双 ...
- C++ 核心指南之资源管理(中)
C++ 核心指南(C++ Core Guidelines)是由 Bjarne Stroustrup.Herb Sutter 等顶尖 C++ 专家创建的一份 C++ 指南.规则及最佳实践.旨在帮助大家正 ...
- 温故知新,signalR、RSA加密、ConcurrentQueue队列
这是一个用户密码异步加解密的Demo,包含了RSA非对称加密,ConcurrentQueue线程安全队列使用,signalR实时推送加解密信息等内容. 说在前面的话 距离上次更新已然快过去一年了,这中 ...
- Junit4 一直处于运行中的排查过程
新买了一个Macbook Pro . 之前的工程搬家过来, 这天要跑个单元测试. 发现Junit4 一直处于运行中.没有错误信息,没有用例执行结果.遂开始排查原因. 这里插一句,苹果芯片的Mbp还是很 ...
- SQL Server中的NULL值处理:判断与解决方案
摘要: 在SQL Server数据库中,NULL是表示缺少数据或未知值的特殊标记.处理NULL值是SQL开发人员经常遇到的问题之一.本文将介绍SQL Server中判断和处理NULL值的不同方法,以及 ...
- OC项目集成flutter后,编译卡死
oc项目集成flutter的项目,本来运行的好好的,突然就再Xcode编译就是卡死的情况, 先运行一下flutter的项目,再编译Xcode的项目,就好了
- 堆栈式 CMOS、背照式 CMOS 和传统 CMOS 传感器的区别
光电效应 光电效应的现象是赫兹(频率的单位就是以他命名的)发现的,但是是爱因斯坦正确解释的.简单说,光或某一些电磁波,照射在某些光敏物质会产生电子,这就是光电效应. 这就将光变为了电,光信号的改变会带 ...