原文在这里

由 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 vetgopls 使用的 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 循环的更多相关文章

  1. TMsgThread, TCommThread -- 在delphi线程中实现消息循环(105篇博客,好多研究消息的文章)

    在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使用很方便,但是有时候我们需要在线程类中使用消息循环,delphi没有提供.   花了两天的事件研究了 ...

  2. Java中的do-while循环——通过示例学习Java编程(11)

    作者:CHAITANYA SINGH 来源:https://www.koofun.com/pro/kfpostsdetail?kfpostsid=22&cid=0 在上一篇教程中,我们讨论了w ...

  3. Fedora 22中的RPM软件包管理工具

    Introduction The RPM Package Manager (RPM) is an open packaging system that runs on Fedora as well a ...

  4. Fedora 22中的用户和用户组管理

    The control of users and groups is a core element of Fedora system administration. This chapter expl ...

  5. Fedora 22中的日期和时间配置

    Introduction Modern operating systems distinguish between the following two types of clocks: A real- ...

  6. Oracle中三种循环(For、While、Loop)

    1.ORACLE中的GOTO用法 DECLARE x number; BEGIN x := 9; <<repeat_loop>> --循环点 x := x - 1; DBMS_ ...

  7. cocos2dx常见的46中+22中动作详解

    cocos2dx常见的46中+22中动作详解 分类: iOS2013-10-16 00:44 1429人阅读 评论(0) 收藏 举报 bool HelloWorld::init(){    ///// ...

  8. TMsgThread, TCommThread -- 在delphi线程中实现消息循环

    http://delphi.cjcsoft.net//viewthread.php?tid=635 在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使 ...

  9. 深入了解JavaScript中的for循环

    在ECMAScript5中,有三种for循环,分别是: 简单for循环 for-in forEach 在ES6中,新增了一种循环 for-of 简单for循环 const arr = [1, 2, 3 ...

  10. 【测试技术】ant中的for循环用法

    有的时候,我们希望ant中也能类似脚本语言一样进行for循环,以实现一些重复性工作.由于ant核心包并未提供此功能,所以需要下载一个扩展包扔到ant的lib目录下去.详细步骤如下: 1.下载核心包:a ...

随机推荐

  1. 一分钟学一个 Linux 命令 - mv 和 cp

    前言 大家好,我是god23bin.欢迎来到<一分钟学一个 Linux 命令>系列,今天需要你花两分钟时间来学习下,因为今天要讲的是两个命令,mv 和 cp 命令. mv 什么是 mv 命 ...

  2. 算法基础(一):串匹配问题(BF,KMP算法)

    好家伙,学算法, 这篇看完,如果没有学会KMP算法,麻烦给我点踩 希望你能拿起纸和笔,一边阅读一边思考,看完这篇文章大概需要(20分钟的时间)   我们学这个算法是为了解决串匹配的问题 那什么是串匹配 ...

  3. windows服务启动时提示找不到指定路径的问题

    我是自己写了一个windows服务,并且在之前一直运行良好,上周四晚上之后,竟然莫名其妙的停止了,我登上远程服务器,才发现,该服务已经停止,当我手动打开该服务时,提示我如下错误,找不到指定路径:. 一 ...

  4. vivo 帐号服务稳定性建设之路-平台产品系列06

    作者:vivo 互联网平台产品研发团队- Shi Jianhua.Sun Song 帐号是一个核心的基础服务,对于基础服务而言稳定性就是生命线.在这篇文章中,将与大家分享我们在帐号稳定性建设方面的经验 ...

  5. 300行代码模拟cdn

    这一生听过许多道理,但还是过不好这一生,这是因为缺少真正的动手实践,光听道理,缺少动手实践的过程,学习难免会让人觉得味同嚼蜡,所以我的分享都比较倾向于实践,在一次次动手实践的过程中感受知识原本纯真的模 ...

  6. CSS3学习记录之loading动画

    loading动画就是在加载一些网页内容的时候呈现出来的小动画,记录一下学到的几种loading动画: 效果:http://39.105.101.122/myhtml/CSS/Loading/load ...

  7. 用AI技术实现自动化的社交媒体广告投放,提高广告效果和收益

    目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 随着社交媒体的普及,广告投放已经成为了广告行业的重要一环.在过去的几年中,社交媒体广告投放的效果和收益都得到了显著提高,但同时也存在着一些 ...

  8. JAVA获取字符串内的括号对;获取括号对的内容;按指定规则返回括号对位置;

    先看结果:处理字符串 "这个是一条测试用的字符串[ ( 5 ( 4( 3 [(1) (2)] ))(7))][(6)]" 结果 解决思路:参考正则表达式里面出入站部分 代码实现如下 ...

  9. iis7以上 ssl 证书导入

    证书导入 开始 -〉运行 -〉MMC: 启动控制台程序,选择菜单"文件"中的"添加/删除管理单元"-> "添加",从"可用的 ...

  10. CentOS 30分钟部署免费在线客服系统

    前段时间我发表了一系列文章,开始介绍基于 .net core 的在线客服系统开发过程.期间有一些朋友希望能够给出 Linux 环境的安装部署指导,本文基于 CentOS 7.9 来安装部署. 我详细列 ...