Go - for range 的陷阱
在 Go 语言中,for range 是一种常用的循环语法,用于遍历数组、切片、映射等集合类型。然而,在使用 for range 时,循环变量的地址变化可能会引发一些意外的行为,尤其是在涉及取地址操作时。以下是详细的说明和分析:
1. for range 的基本行为
在 for range 循环中,每次迭代时,循环变量(如 value)实际上是集合中元素的副本,而不是原始元素的引用。这意味着对循环变量的修改不会影响原始集合中的元素。
2. 地址变化的特性
2.1 循环变量的地址
在 for range 循环中,每次迭代时,循环变量的地址可能是相同的,也可能是不同的,这取决于 Go 的版本和具体实现:
Go 1.22 之前:循环变量的地址在每次迭代时是相同的。这是因为循环变量是同一个局部变量的重复赋值。
Go 1.22 之后:循环变量的地址在每次迭代时可能会发生变化。这是因为 Go 在 1.22 版本后对 for range 的实现进行了优化,使得每次迭代都会创建一个新的变量。
2.2 原始元素的地址
无论 Go 的版本如何,for range 中的循环变量地址与原始集合中元素的地址始终是不同的。这是因为循环变量是原始元素的副本,而不是引用。
3. 常见问题与陷阱
3.1 地址引用问题
在 for range 中对循环变量取地址并存储时,可能会导致意外的结果。例如:
1 |
|
上述代码中,m 中存储的地址指向的是循环变量 v 的地址,而 v 在循环结束后指向了最后一个元素的副本。
在 Go1.22 之前,(测试时使用的 Go1.20.4)会输出如下:
1 |
|
而在 1.22 之后,(测试时使用的 Go 1.23.2)会输出如下:
1 |
|
可以看到,在 Go 1.22 之后,forrange 循环中的循环变量的地址在每次迭代的时候会发生变化,从而避免了地址引用的问题,m 的每一个元素的值都不一样了
3.2 闭包问题
在 for range 中使用闭包时,也可能出现类似的问题。例如:
1 |
|
在 Go 1.22 之前,闭包中捕获的变量 i 是循环变量的引用,在每次循环中地址不会发生变化,最终的值是最后一次循环结束后 i 的值 5,这就会导致所有闭包输出相同的值 5:
1 |
|
但在 Go 1.22 之后,这一问题已被修复,每次循环重新分配循环变量的内存
1 |
|
4. 解决方案
为了避免 for range 中的地址引用问题,可以采取以下方法:
4.1 直接使用索引访问原始元素
1 |
|
4.2 在循环中创建新的变量副本
在循环中创建新的变量副本的时候会给新副本分配内存,从而避免了地址重用的问题
1 |
|
5. 性能分析
从性能角度看,for range 的性能与普通 for 循环相当,但在处理复杂类型(如大结构体)时,由于每次迭代都会创建副本,可能会导致额外的性能开销。如果需要优化性能,可以考虑直接使用索引访问集合元素。
6. 总结
在 for range 中,循环变量是原始元素的副本,其地址可能在每次迭代时相同(Go 1.22 之前)或不同(Go 1.22 之后)。
循环变量的地址与原始集合中元素的地址始终不同。
在涉及取地址或闭包时,需特别注意循环变量的地址特性,以避免潜在的陷阱。