230 lines
5.0 KiB
Markdown
230 lines
5.0 KiB
Markdown
---
|
||
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 // 直接跳出外层循环
|
||
> }
|
||
> }
|
||
> }
|
||
> ```
|