Common Go Mistakes(Ⅲ 控制结构)
导航
- 常见的 Go 错误
- 引言
- Ⅲ 控制结构
- 30. 忽略 `range` 循环变量是一个拷贝
- 31. 忽略 range 循环中迭代目标值的计算方式
- 32. 忽略 range 循环中指针元素的影响
- 33. map 迭代过程中的错误假设
- 34. 忽略 break 语句如何工作
- 35. 在循环中使用 defer
常见的 Go 错误
参考 100go
引言
- 在循环中如何处理指针
- break 语句
Ⅲ 控制结构
30. 忽略 range 循环变量是一个拷贝
这个问题是Go语言中range循环的一个常见陷阱:range循环中的元素是值拷贝,而不是引用。
当使用range遍历切片或数组时,循环变量获得的是元素的副本,而不是原始元素的引用。如果试图通过循环变量修改元素,实际上只是修改了副本,原始数据不会改变。
示例代码
type User struct {Name stringAge int
}func main() {users := []User{{"Alice", 25},{"Bob", 30},}// 错误示例:试图修改元素for _, u := range users {u.Age = u.Age + 1 // 只修改了副本,原数据不变}fmt.Println(users) // 输出: [{Alice 25} {Bob 30}],没有变化
}
解决方案
方案1:使用索引访问原始元素
for i := range users {users[i].Age = users[i].Age + 1
}
方案2:使用指针切片
userPtrs := []*User{{"Alice", 25},{"Bob", 30},
}for _, u := range userPtrs {u.Age = u.Age + 1 // 通过指针修改原始数据
}
为什么会这样设计?
- 值拷贝是Go语言的一致性设计,避免隐式的引用传递
- 对于大结构体,这可能会有性能影响
- 如果需要修改原始数据,应该明确使用索引或指针
31. 忽略 range 循环中迭代目标值的计算方式
在Go语言,range 关键字后面的表达式在循环开始之前,只会被求值一次,一旦循环开始,它遍历的是这个表达式在循环开始时所确定的值或状态。例如:
// 示例:arr 数组是 Range 表达式
arr := [3]int{1, 2, 3} for i, v := range arr { // arr 表达式只在循环开始前求值一次arr[2] = 99 // 在循环体内修改原始数组fmt.Printf("Iteration %d: v=%d\n", i, v)
}// 输出结果:
// Iteration 0: v=1
// Iteration 1: v=2
// Iteration 2: v=3
遍历不同的对象时有不同的表现,如下所示:
| 类型 | 求值次数 | 循环遍历的对象 | 循环内修改原始集合的影响 |
|---|---|---|---|
| Array (数组) | 一次 | 数组的完整副本 | 原始数组被修改,但循环不受影响。 |
| Slice (切片) | 一次 | 切片头的副本(指向原始底层数组) | 通过索引修改底层数组会生效,但循环变量 v 依然是旧值的副本。 |
| Map (映射) | 一次 | 映射的引用副本 | 循环内删除元素有效;添加元素不保证被遍历。 |
32. 忽略 range 循环中指针元素的影响
在range循环中,循环变量(如v)在每次迭代时会被重新赋值,但变量本身的内存地址不变。如果取这个变量的地址&v,所有指针都会指向同一个内存位置。
Go 1.22 之前的 for 循环迭代变量是 per-variable-per-loop 而不是 per-variable-per-iteration,才导致该问题。
Go 1.22+改进了这个问题,在这之后的版本不存在该问题。详见
33. map 迭代过程中的错误假设
在 Go 语言中,for range 循环遍历映射(map)时,容易产生以下错误的假设:
- 映射会按照键的顺序(或插入顺序)进行遍历
- 在迭代过程中添加的元素,会在当前循环中被遍历到
- 每次遍历顺序相同的
34. 忽略 break 语句如何工作
在 Go 语言中,一个不带标签(Label)的 break 语句只能中断它所处的最内层的 for、switch 或 select 语句。代码如下:
package mainimport ("context""fmt"
)func listing1() {for i := 0; i < 5; i++ {fmt.Printf("%d ", i)switch i {default:case 2:break}}
}func listing2() {
loop:for i := 0; i < 5; i++ {fmt.Printf("%d ", i)switch i {default:case 2:break loop}}
}func listing3(ctx context.Context, ch <-chan int) {for {select {case <-ch:// Do somethingcase <-ctx.Done():break}}
}func listing4(ctx context.Context, ch <-chan int) {
loop:for {select {case <-ch:// Do somethingcase <-ctx.Done():break loop}}
}
35. 在循环中使用 defer
defer 语句是基于函数的,而不是基于循环的。 在循环中使用 defer 会导致资源(如文件句柄、锁、内存)得不到及时释放,引发资源泄漏或内存溢出(OOM)。
当在循环内部使用 defer 来处理资源(例如打开文件、获取数据库连接、申请锁)时,关闭动作必须等到整个函数退出时才会发生。
可以通过新建一个函数或使用匿名函数来解决该问题,例如:
package mainimport "os"func readFiles1(ch <-chan string) error {for path := range ch {file, err := os.Open(path)if err != nil {return err}defer file.Close()// Do something with file}return nil
}func readFiles2(ch <-chan string) error {for path := range ch {if err := readFile(path); err != nil {return err}}return nil
}func readFile(path string) error {file, err := os.Open(path)if err != nil {return err}defer file.Close()// Do something with filereturn nil
}func readFiles3(ch <-chan string) error {for path := range ch {err := func() error {file, err := os.Open(path)if err != nil {return err}defer file.Close()// Do something with filereturn nil}()if err != nil {return err}}return nil
}
