原文在这里

由 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. 混合编程python与C++

    上个版本: 只是用到ctypes进行传输, 这次将python服务端更改为C++服务端,方便后续维护. 本文实现功能: python传输图片给C++, C++接受图片后对图片进行处理,并将结果返回给p ...

  2. 逍遥自在学C语言 | 指针的基础用法

    前言 在C语言中,指针是一项重要的概念,它允许我们直接访问和操作内存地址. 可以说,指针是C语言一大优势.用得好,你写程序如同赵子龙百万军中取上将首级:用得不好,则各种问题层出不穷,有种双拳难敌四手的 ...

  3. Thinkphp6 连接达梦数据库

    Thinkphp6 连接达梦数据库 这里使用 IDEA phpEnv PHP7.3 Thinkphp6 桌面操作系统:Windows11 虚拟机:VMware 服务器操作系统:银河麒麟 在虚拟机操作与 ...

  4. P7561[JOISC 2021 Day2] 道路の建設案 (Road Construction) 题解

    P7561[JOISC 2021 Day2] 道路の建設案 (Road Construction) 题解 题目描述 JOI 国是一个 \(x\times y\) 的二维平面,王国里有 \(n\) 个城 ...

  5. 数据库连接池之c3p0-0.9.1.2,16年的古董,发生连接泄露怎么查(一)

    背景 这篇文章是写给有缘人的,为什么这么说呢,因为本篇主要讲讲数据库连接池之c3p0-0.9.1.2版本. 年轻的朋友,可能没怎么听过c3p0了,或者也仅限于听说,这都很正常,因为c3p0算是200几 ...

  6. 博客代码托管网站个人体会及感受(GitHub、Coding、Netlity、阿里云弹性web托管)

    GitHub 免费 部署 github上,服务器在国外,访问速度一般,稳定性比较好,网站知名,操作方便,部署简单,域名不需要备案. Coding 免费 coding 支持 PHP + mysql 的动 ...

  7. Linux 命令:btrfs filesystem resize

    btrfs filesystem resize 2:300G /path ## 为创建了btrfs文件系统,已经挂载到/path 且device ID为2的硬盘/分区进行resize # 已经做过硬盘 ...

  8. Mysql高级2-SQL性能分析

    一.SQL执行频率 MySQL客户端 连接成功后,通过show [session | global] status 命令可以提供服务器状态信息,通过如下指令,可以查看当前数据库的insert,upda ...

  9. 【算法】单调栈 & 单调队列学习笔记

    1. 单调栈简介 1.1 前言 今天是 2023/1/15,一中寒假集训阶段性的结束了.集训的学习笔记可以在本人 blogs 的[算法]标签栏中找. 马上就要过年了,提前祝大家新年快乐! 1.2 什么 ...

  10. 快速掌握Vue3:速成Vue3前端开发看这篇就够啦

    一.Vue基本概念 1.1-Vue3的优点 Vue3支持Vue2额大多数特性. 更好的支持TypeScript. 打包大小减少41%. 初次渲染快55%,更新渲染快133%. 内存减少54%. 使用p ...