Go的for range 遍历切片时有什么坑?
Go 语言中的 for range 语法非常简洁,但在遍历切片(Slice)时,由于其底层的实现机制,存在几个经典的“坑”。
以下是主要的 5 个坑及其解决方案:
1. 坑一:修改 value 不会影响原切片(值拷贝)
这是新手最容易犯的错误。for i, v := range slice 中的 v 是切片元素的副本(值拷贝)。修改 v 的值,原本切片中的数据不会发生变化。
错误示例:
type User struct {
Name string
Age int
}
users := []User{{"Alice", 20}, {"Bob", 30}}
for _, u := range users {
u.Age = 18 // 这里的 u 只是一个拷贝
}
fmt.Println(users) // 输出仍然是 [{Alice 20} {Bob 30}],修改无效
正确做法: 使用索引直接修改原切片。
for i := range users {
users[i].Age = 18 // 直接通过索引修改
}
或者,如果切片里存的是指针 []*User,那么 v 拷贝的是指针地址,修改 v 指向的内容是有效的。
2. 坑二:循环变量的地址复用(Go 1.22 之前)
注意:这个问题在 Go 1.22 版本中已经修复(实验性特性 GOEXPERIMENT=loopvar 转正),但在 Go 1.21 及更早版本中非常致命。
在旧版本中,for 循环的变量 v 在整个循环过程中是同一个变量,只是每次迭代时值被更新了。如果你在循环中获取 v 的地址(&v),你会发现所有地址都是一样的。
错误示例(Go < 1.22):
arr := []int{1, 2, 3}
var ptrs []*int
for _, v := range arr {
ptrs = append(ptrs, &v) // 取的是 v 的地址
}
for _, p := range ptrs {
fmt.Print(*p, " ")
}
// Go < 1.22 输出: 3 3 3 (所有指针都指向了 v 最后一次迭代的值)
// Go >= 1.22 输出: 1 2 3 (新版本每次迭代 v 都是新变量)
正确做法(兼容旧版本): 在循环内部定义一个新变量承接。
for _, v := range arr {
v := v // 影子变量(Shadowing),开辟新内存
ptrs = append(ptrs, &v)
}
3. 坑三:在 Goroutine 中使用循环变量(闭包捕获)
这其实是“地址复用”的衍生问题。如果在循环中启动 Goroutine 并直接使用循环变量,由于 Goroutine 执行的延迟性,当它运行时,循环可能已经结束,变量变成了最后一个元素的值。
错误示例(Go < 1.22):
values := []int{1, 2, 3}
for _, v := range values {
go func() {
fmt.Println(v) // 闭包捕获了 v
}()
}
// 很大概率输出: 3 3 3
正确做法: 通过传参或局部变量赋值。
for _, v := range values {
go func(val int) {
fmt.Println(val)
}(v) // 将 v 作为参数传入,发生值拷贝
}
注:同样,Go 1.22 修复了这个问题,直接写错误示例的代码也能得到预期结果,但为了兼容性通常建议显式传参。
4. 坑四:大结构体的性能损耗
由于 for _, v := range slice 会发生值拷贝,如果切片中的元素是很大的结构体(例如包含大数组),每次迭代都会进行一次内存复制,这会带来不必要的性能开销。
低效代码:
type BigStruct struct {
Data [1024 * 1024]byte // 1MB 大小
}
data := make([]BigStruct, 1000)
for _, v := range data {
// 每次循环都会拷贝 1MB 数据到 v
_ = v
}
优化做法: 只遍历索引,或者使用切片指针。
// 方式1:只用索引,不读取 value
for i := range data {
_ = data[i]
}
// 方式2:切片里存指针 []*BigStruct
5. 坑五:Range 表达式只计算一次
for range 后面的切片表达式是在循环开始前只计算一次长度的。这意味着如果在循环体内向切片 append 元素,循环次数不会增加,不会变成死循环。
示例:
v := []int{1, 2, 3}
for i := range v {
v = append(v, i) // 在循环中追加元素
}
fmt.Println(v)
// 输出: [1 2 3 0 1 2]
// 循环只执行了 3 次(原切片长度),并没有因为 append 而无限循环。
总结
- 想修改原切片? 用
slice[i],别用v。 - 存指针或开协程? 小心
v的地址复用(Go 1.22 前需局部赋值或传参)。 - 元素很大? 用
for i := range避免拷贝。 - 循环内追加? 放心,不会死循环,次数由起始长度决定。