Files
cs-note/hzh/GolangStar/Go语言基础/Go语言循环.md
T

230 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 // 直接跳出外层循环
> }
> }
> }
> ```