百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Go 1.22终于修复了for循环中的变量问题

wptr33 2025-03-25 18:09 22 浏览

Go 1.21 包含了一个对循环作用域的变更的预览,我们计划在 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 的常见问题解答中的条目 "闭包在作为 goroutine 运行时会发生什么?" 给出了这个例子,并指出 "使用闭包与并发时可能会导致一些混淆"。

尽管通常涉及并发,但并非必须如此。这个例子有相同的问题,但没有 goroutine:

func main() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

这种错误已经在许多公司引起了生产问题,包括1619047 - Let's Encrypt: CAA Rechecking bug的问题。在那种情况下,循环变量的意外捕获跨越多个函数,并且更难以注意到:

// 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使用的循环闭包分析器选择了误报,只有在确定存在问题时才报告,而漏报其他情况。其他检查器选择了误报,将正确的代码指责为错误。我们对添加了"x := x"行的开源Go代码进行了分析,期望找到bug修复。然而,我们发现许多不必要的行被添加进去,这表明流行的检查器存在相当高的误报率,但开发人员仍然添加这些行以保持检查器的满意。

我们找到的一对例子特别有启发性:

这个diff是在一个程序中的:

for _, informer := range c.informerMap {
+        informer := informer
         go informer.Run(stopCh)
     }

而这个diff是在另一个程序中的:

  for _, a := range alarms {
+        a := a
         go a.Monitor(b)
     }

其中一个diff是一个bug修复,另一个是一个不必要的更改。除非你了解涉及的类型和函数的更多信息,否则无法确定哪个是哪个。

修复方法

对于Go 1.22,我们计划修改for循环,使这些变量具有每次迭代的作用域,而不是每个循环的作用域。这个改变将修复上述例子,使它们不再是有bug的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

我们在谷歌的内部 Go 工具链中修复了此模式,并在 2023 年 5 月初开始的所有构建中强制使用该模式,在过去的四个月中,我们在生产代码中没有收到任何问题的报告。

您还可以尝试在 Go Playground 上包含一个 // GOEXPERIMENT=loopvar 的注释,以更好地理解语义,例如在这个程序中(Go Playground - The Go Programming Language)。请注意,这个注释仅适用于 Go Playground

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...