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
2
3
4
5
6
7
8
9
arr := []int{1, 2, 3}
m := make(map[int]*int)
for i, v := range arr {
fmt.Println(&v)
m[i] = &v
}
for _, v := range m {
fmt.Println(*v) // 返回这个地址对应的值,输出 3 3 3
}

上述代码中,m 中存储的地址指向的是循环变量 v 的地址,而 v 在循环结束后指向了最后一个元素的副本

在 Go1.22 之前,(测试时使用的 Go1.20.4)会输出如下:

1
2
3
4
5
6
7
8
9
=== RUN   TestRange
0x1400009a1d8
0x1400009a1d8
0x1400009a1d8
3
3
3
--- PASS: TestRange (0.00s)
PASS

而在 1.22 之后,(测试时使用的 Go 1.23.2)会输出如下:

1
2
3
4
5
6
7
8
9
=== RUN   TestRange
0x1400000e328
0x1400000e330
0x1400000e338
1
2
3
--- PASS: TestRange (0.00s)
PASS

可以看到,在 Go 1.22 之后,forrange 循环中的循环变量的地址在每次迭代的时候会发生变化,从而避免了地址引用的问题,m 的每一个元素的值都不一样了

3.2 闭包问题

在 for range 中使用闭包时,也可能出现类似的问题。例如:

1
2
3
4
5
6
7
8
9
funcs := make([]func(), 0, 10)
for i := 0; i < 5; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f()
}

在 Go 1.22 之前,闭包中捕获的变量 i 是循环变量的引用,在每次循环中地址不会发生变化,最终的值是最后一次循环结束后 i 的值 5,这就会导致所有闭包输出相同的值 5:

1
2
3
4
5
6
7
8
=== RUN   TestRange
5
5
5
5
5
--- PASS: TestRange (0.00s)
PASS

但在 Go 1.22 之后,这一问题已被修复,每次循环重新分配循环变量的内存

1
2
3
4
5
6
7
8
=== RUN   TestRange
0
1
2
3
4
--- PASS: TestRange (0.00s)
PASS

4. 解决方案

为了避免 for range 中的地址引用问题,可以采取以下方法:

4.1 直接使用索引访问原始元素

1
2
3
4
5
arr := []int{1, 2, 3}
m := make(map[int]*int)
for i := range arr {
m[i] = &arr[i]
}

4.2 在循环中创建新的变量副本

在循环中创建新的变量副本的时候会给新副本分配内存,从而避免了地址重用的问题

1
2
3
4
5
6
arr := []int{1, 2, 3}
m := make(map[int]*int)
for _, v := range arr {
temp := v
m[v] = &temp
}

5. 性能分析

从性能角度看,for range 的性能与普通 for 循环相当,但在处理复杂类型(如大结构体)时,由于每次迭代都会创建副本,可能会导致额外的性能开销。如果需要优化性能,可以考虑直接使用索引访问集合元素。

6. 总结

在 for range 中,循环变量是原始元素的副本,其地址可能在每次迭代时相同(Go 1.22 之前)或不同(Go 1.22 之后)。

循环变量的地址与原始集合中元素的地址始终不同。

在涉及取地址或闭包时,需特别注意循环变量的地址特性,以避免潜在的陷阱。


Go - for range 的陷阱
https://wuwanhao.github.io/2025/03/27/Go - for range的陷阱/
作者
Wuuu
发布于
2025年3月27日
许可协议