--- tags: [go, golang, go基础语法, 循环] create time: 2026-06-07 15:00 --- # Go 语言循环 ## 概述 Go 只有一种循环关键字 `for`,但它能覆盖所有常见循环场景。本文介绍三种 for 形式、`for range` 用法,以及几个经典的 "range 陷阱"。 ## 正文 ### Go 为什么只有 for? 在 C/Java 中有 `for`、`while`、`do-while` 三种循环,而 Go 选择**统一用 `for`**。这不是限制,而是简化——三种写法语义清晰,不需要多个关键字来增加记忆负担。 > [!question] ❓ 思考 > 如果只用一个关键字实现所有循环,你觉得设计者可能考虑了什么? ### 三种 for 形式 #### 形式一:经典 for(C 风格) ```go for init; condition; post { // 初始化 → 判断 → 执行 → 步进 → 重复判断 } // 示例:打印 0~4 for i := 0; i < 5; i++ { fmt.Println(i) } ``` 三部分都可以省略,但分号不能省(省略时分号也隐式存在)。 #### 形式二:while 风格 ```go for condition { // 只有条件判断,无 init 和 post } j := 0 for j < 5 { fmt.Println(j) j++ } ``` #### 形式三:无限循环 ```go for { // 永远执行,需要内部 break } ``` > [!note] 📝 三者等价关系 > - `for init; cond; post {}` ≈ C 的 `for` > - `for cond {}` ≈ C 的 `while` > - `for {}` ≈ C 的 `for(;;)` ### for range:遍历集合的优雅方式 `for range` 可以遍历 slice、map、array、string、channel: ```go // slice for i, v := range []int{1, 2, 3} { fmt.Printf("index:%d value:%d\n", i, v) } // map for k, v := range map[string]int{"a": 1, "b": 2} { fmt.Printf("%s:%d\n", k, v) } // 只取 index for i := range slice { ... } // 只取 value for _, v := range slice { ... } ``` > [!tip] 💡 省略技巧 > `_` 是空白标识符,用来丢弃不需要的值。`for _, v := range` 是最常见的写法之一。 ### ⚠️ for range 的经典陷阱 #### 陷阱 1:闭包捕获迭代变量 ```go var funcs []func() for i := 0; i < 3; i++ { funcs = append(funcs, func() { fmt.Println(i) // 所有闭包共享同一个 i }) } for _, f := range funcs { f() // 输出:3 3 3 } ``` **原因**:`i` 在整个循环中是同一个变量,闭包捕获的是它的**引用**而非值。循环结束后 `i == 3`,所以所有闭包都打印 3。 **修复方法**:在循环内创建局部副本: ```go for i := 0; i < 3; i++ { i := i // 这一行创建了一个新的局部变量 i funcs = append(funcs, func() { fmt.Println(i) // 现在每个闭包捕获的是各自的 i }) } // 输出:0 1 2 ``` > [!warning] ⚠️ Go 1.22+ 已修复 > Go 1.22 起,`for range` 的迭代变量每次循环都会重新创建,上述问题不再出现。但如果要兼容旧版本,仍需用 `i := i` 手法。 #### 陷阱 2:修改迭代变量不影响原切片 ```go slice := []int{1, 2, 3} for _, v := range slice { v *= 10 // 修改的是副本 } fmt.Println(slice) // [1 2 3] — 没变! ``` **修复**:用索引访问原始元素: ```go for i := range slice { slice[i] *= 10 } fmt.Println(slice) // [10 20 30] ✅ ``` #### 陷阱 3:遍历中追加元素不会扩展循环次数 ```go v := []int{1, 2, 3} for i := range v { v = append(v, i) // 循环仍然只执行 3 次 } ``` `for range` 在循环开始前就确定了长度,后续 `append` 不会影响迭代次数。 #### 陷阱 4:Map 遍历顺序随机 ```go m := map[string]int{"a": 1, "b": 2, "c": 3} for k, v := range m { fmt.Println(k, v) // 每次运行顺序可能不同 } ``` 如果需要有序输出,先收集 key 再排序: ```go keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println(k, m[k]) } ``` ### 字符串遍历:rune vs byte `for range` 遍历字符串时返回的是 **Unicode 代码点(rune)**,不是字节: ```go str := "hello 世界" for i, r := range str { fmt.Printf("index: %d, rune: %c\n", i, r) } // '世' 占 3 个字节,index 从 5 开始 ``` 如果需要按字节遍历,用传统 for: ```go for i := 0; i < len(str); i++ { fmt.Printf("byte: %x\n", str[i]) } ``` > [!info] ℹ️ string 底层结构 > Go 的 string 是只读的字节序列。UTF-8 编码下,ASCII 字符占 1 字节,中文占 3 字节。`for range` 自动处理 UTF-8 解码,返回完整的 Unicode 码点。 ### 循环控制语句 | 语句 | 作用 | |------|------| | `break` | 跳出当前循环 | | `continue` | 跳过本次迭代,进入下一次 | | `goto` | 跳转到标签(慎用) | ```go for i := 0; i < 10; i++ { if i == 5 { break // 遇到 5 停止 } if i%2 == 0 { continue // 跳过偶数 } fmt.Println(i) // 只打印奇数 } ``` > [!tip] 💡 带标签的 break > 跳出嵌套循环时,可以用标签指定跳出哪个循环: > ```go > Outer: > for i := 0; i < 3; i++ { > for j := 0; j < 3; j++ { > if i == 1 && j == 1 { > break Outer // 直接跳出外层循环 > } > } > } > ```