Go 1.22 中的 For 循环

go,for,循环 · 浏览次数 : 155

小编点评

**Go 1.22 中的 for 循环作用域更改** Go 1.22 中的 for 循环将采用作用域更改,以解决在迭代结束后仍然保留对循环变量的引用的问题。 **作用域更改的原理** 作用域更改是指在每次循环中,创建并释放一个新的作用域。这有助于避免对循环变量的意外捕获,并确保每个子测试在独立的作用域中运行。 **示例** 以下代码展示了作用域更改的用法: ```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 } } ``` 在这个例子中,`for` 循环会创建一个新的作用域,并在每个子测试中创建一个新的变量 `v`。这确保在循环结束后,变量 `v` 的值已从作用域中释放,避免对 `done` 的意外访问。

正文

原文在这里

由 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 循环相似的内容:

Go 1.22 中的 For 循环

原文在这里。 由 David Chase and Russ Cox 发布于2023年9月19日 Go 1.21 版本包含了对 for 循环作用域的预览更改,我们计划在 Go 1.22 中发布此更改,以消除其中一种最常见的 Go 错误。 问题 如果你写过一定量的 Go 代码,你可能犯过一个错误,即在迭

2024-05-22:用go语言,你有一个包含 n 个整数的数组 nums。 每个数组的代价是指该数组中的第一个元素的值。 你的目标是将这个数组划分为三个连续且互不重叠的子数组。 然后,计算这三个子数

2024-05-22:用go语言,你有一个包含 n 个整数的数组 nums。 每个数组的代价是指该数组中的第一个元素的值。 你的目标是将这个数组划分为三个连续且互不重叠的子数组。 然后,计算这三个子数组的代价之和, 要求返回这个和的最小值。 输入:nums = [1,2,3,12]。 输出:6。 答

go 流程控制之switch 语句介绍

go 流程控制之switch 语句介绍 目录go 流程控制之switch 语句介绍一、switch语句介绍1.1 认识 switch 语句1.2 基本语法二、Go语言switch语句中case表达式求值顺序2.1 switch语句中case表达式求值次序介绍2.2 switch语句中case表达式的

Ollama开发指南

安装必备工具 确保已安装以下软件的正确版本: CMake 3.24 或更高版本 Go 1.22 或更高版本 GCC 11.4.0 或更高版本 使用 Homebrew 安装这些工具(适用于macOS和Linux): brew install go cmake gcc 可选:启用调试与详细日志 构建时开

Go pprof 认知到实践

快速开始 测试环境:go version go1.22.2 windows/amd64,源代码开源在 https://github.com/oldme-git/teach-study/tree/master/golang/base/pprof 在正式开始之前,请确保安装 graphviz,这一步不可

深入解析Go非类型安全指针:技术全解与最佳实践

本文全面深入地探讨了Go非类型安全指针,特别是在Go语言环境下的应用。从基本概念、使用场景,到潜在风险和挑战,文章提供了一系列具体的代码示例和最佳实践。目的是帮助读者在保证代码安全和效率的同时,更加精通非类型安全指针的使用。 关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识

Go 单元测试基本介绍

目录引入一、单元测试基本介绍1.1 什么是单元测试?1.2 如何写好单元测试1.3 单元测试的优点1.4 单元测试的设计原则二、Go语言测试2.1 Go单元测试概要2.2 Go单元测试基本规范2.3 一个简单例子2.3.1 使用Goland 生成测试文件2.3.2 运行单元测试2.3.3 完善测试用

Go 复合类型之字典类型介绍

Go 复合类型之字典类型介绍 目录Go 复合类型之字典类型介绍一、map类型介绍1.1 什么是 map 类型?1.2 map 类型特性二.map 变量的声明和初始化2.1 方法一:使用 make 函数声明和初始化(推荐)2.2 方法二:使用复合字面值声明初始化 map 类型变量三.map 变量的传递

Go 基础之基本数据类型

Go 基础之基本数据类型 目录Go 基础之基本数据类型一、整型1.1 平台无关整型1.1.1 基本概念1.1.2 分类有符号整型(int8~int64)无符号整型(uint8~uint64)1.2 平台相关整型1.2.1 基本概念1.2.2 注意点1.2.3 获取三个类型在目标运行平台上的长度1.3

containerd 源码分析:创建 container(三)

文接 containerd 源码分析:创建 container(二) 1.2.2.2 启动 task 上节介绍了创建 task,task 创建之后将返回 response 给 ctr。接着,ctr 调用 task.Start 启动容器。 // containerd/client/task.go fu