博客中搜索 循环变量
loopvar
https://mp.weixin.qq.com/s/OnvniPEvDz6k5LLKGH1MFA 不再共享循环变量
Implicit memory aliasing in for loop.
目前go vet有一个检测项,会检测出这种情况并 loop variable up captured by func literal.
但是因为go 1.22已经修改,不会再有这种情况出现,所以其实可以加一个判断,对于Go 1.22及以上的版本,不进行这个检测项目
关于闭包捕获循环变量 问题
在 Go 中,闭包捕获循环变量是一个常见的陷阱。这个问题通常出现在你在循环中启动 goroutine 或者定义匿名函数时,它们捕获了共享的循环变量,而不是每次迭代时的具体值。
问题举例
下面是一个会导致 loop variable captured by func literal 问题的简单示例:
1 | package main |
问题原因
在上面的代码中,num 是循环变量,每次循环时它的值都会改变。然而,go func() 中的匿名函数实际上捕获了 num 的引用,而不是它的值。由于 goroutine 并不会立即执行,所有的 goroutine 捕获的 num 都是同一个引用(即循环结束后的 num 值)。
因此,所有的 goroutine 打印的值将是循环结束时的最后一个值,也就是 3。
输出结果
1 | 3 |
所有的 goroutine 都打印 3,而不是 1, 2, 3。
正确的做法
我们需要确保每个 goroutine 捕获的是当前迭代时的 num 值,而不是共享的循环变量。可以通过将 num 作为参数传递给匿名函数,确保每次迭代时的值是独立的。
修改后的代码:
1 | package main |
解释
numCopy := num:在每次迭代中,创建当前num的副本numCopy。这样每个 goroutine 都会捕获到当前迭代时的numCopy,而不是共享的num。go func(n int):将numCopy传递给匿名函数,确保 goroutine 捕获的是numCopy的值。
修改后的输出结果
1 | 1 |
每个 goroutine 都打印了正确的数值。
总结
- 问题:在循环中启动 goroutine 时,匿名函数会捕获共享的循环变量引用,而不是每次迭代时的具体值。这会导致所有 goroutine 打印相同的值。
- 解决方法:通过将循环变量的副本传递给匿名函数,确保每个 goroutine 捕获到的是当前迭代时的值,而不是共享的引用。
对于go 1.22及以后: https://go.dev/play/p/B2KxciPgtPu?v=goprev
当前go.mod中go的版本是1.22.6
移除go.mod中对go版本的指定(不然一直downloading go 1.22.6)
输出和go 1.22.6完全不一样,并且go vet能够检测到~
看来go vet是做了什么文章,不需要我搞了…
https://github.com/cuishuang/loopvar
loopclosure.Analyzer这个分析器是2018年11月份加的
是2024年2月份加的…和go 1.22基本同期更新
1 | // Before reports whether the file version v is strictly before a Go release. |
1 | // Compare returns -1, 0, or +1 depending on whether |
这段代码定义了一个名为 Compare 的函数,用于比较两个 Go 语言的版本号,并返回一个整数来表示它们的关系。具体来说,Compare 函数会根据 Go 版本号的大小返回 -1、0 或 +1,分别表示:
-1:表示x小于y。0:表示x等于y。+1:表示x大于y。
详细说明
函数签名:
1
func Compare(x, y string) int
x和y是两个字符串,表示两个 Go 语言的版本号。- 返回值是一个
int,用于表示两个版本号之间的比较结果:-1:x小于y0:x等于y+1:x大于y
版本号格式要求:
- 版本号必须以
"go"作为前缀,例如:"go1.21",而不是单纯的"1.21"。 - 不符合版本格式的字符串(如空字符串)会被视为无效版本,且无效版本会被认为比任何有效版本都小。
- 版本号必须以
版本比较的规则:
- 普通版本号:例如
"go1.21"。 - 候选版本:例如
"go1.21rc1",这是发布候选版本,通常比最终版本要小。 - 补丁版本:例如
"go1.21.0",代表正式发布的版本。 - 自定义后缀:如果版本号中包含自定义后缀(如
"go1.21.0-bigcorp"),在比较时将忽略这些后缀。也就是说,"go1.21.0"和"go1.21.0-bigcorp"被认为是相等的。
- 普通版本号:例如
实现细节:
Compare函数内部调用了compare函数(未展示),而compare函数实际上是进行版本比较的核心。- 版本号在比较之前,首先会通过
stripGo函数(未展示)去掉前缀"go",然后再进行比较。stripGo可能会将"go1.21"变成"1.21",以便在compare函数中更容易处理。
补充说明
为了更好地理解,可以模拟 Compare 函数的行为:
相同版本:
1
Compare("go1.21.0", "go1.21.0") // 返回 0
不同版本:
1
2Compare("go1.21", "go1.22") // 返回 -1,因为 "go1.21" 小于 "go1.22"
Compare("go1.22", "go1.21") // 返回 1,因为 "go1.22" 大于 "go1.21"带有候选版本的比较:
1
2Compare("go1.21", "go1.21rc1") // 返回 -1,因为 "go1.21" < "go1.21rc1"
Compare("go1.21rc1", "go1.21.0") // 返回 -1,因为 "go1.21rc1" < "go1.21.0"带有自定义后缀的版本比较:
1
Compare("go1.21.0", "go1.21.0-bigcorp") // 返回 0,因为自定义后缀被忽略
总结
这段代码的功能是比较两个 Go 语言的版本号,并且遵循一些特定的规则:
- 必须以
"go"开头。 - 忽略版本号中的自定义后缀。
- 无效的版本号被视为小于任何有效版本号。
Compare 函数通过调用 stripGo 去掉 "go" 前缀后,使用 compare 来完成具体的版本比较逻辑。
所以但凡 go vet出现loop variable up captured by func literal的,可以认为go.mod里指定的go版本都低于go 1.22
但凡是go版本大于等于go 1.22, 都不会进行该项的检测
原文链接: https://dashen.tech/2018/09/18/Go闭包捕获循环变量问题/
版权声明: 转载请注明出处.