5.0 KiB
tags, create time
| tags | 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 风格)
for init; condition; post {
// 初始化 → 判断 → 执行 → 步进 → 重复判断
}
// 示例:打印 0~4
for i := 0; i < 5; i++ {
fmt.Println(i)
}
三部分都可以省略,但分号不能省(省略时分号也隐式存在)。
形式二:while 风格
for condition {
// 只有条件判断,无 init 和 post
}
j := 0
for j < 5 {
fmt.Println(j)
j++
}
形式三:无限循环
for {
// 永远执行,需要内部 break
}
[!note] 📝 三者等价关系
for init; cond; post {}≈ C 的forfor cond {}≈ C 的whilefor {}≈ C 的for(;;)
for range:遍历集合的优雅方式
for range 可以遍历 slice、map、array、string、channel:
// 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:闭包捕获迭代变量
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。
修复方法:在循环内创建局部副本:
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:修改迭代变量不影响原切片
slice := []int{1, 2, 3}
for _, v := range slice {
v *= 10 // 修改的是副本
}
fmt.Println(slice) // [1 2 3] — 没变!
修复:用索引访问原始元素:
for i := range slice {
slice[i] *= 10
}
fmt.Println(slice) // [10 20 30] ✅
陷阱 3:遍历中追加元素不会扩展循环次数
v := []int{1, 2, 3}
for i := range v {
v = append(v, i) // 循环仍然只执行 3 次
}
for range 在循环开始前就确定了长度,后续 append 不会影响迭代次数。
陷阱 4:Map 遍历顺序随机
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v) // 每次运行顺序可能不同
}
如果需要有序输出,先收集 key 再排序:
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),不是字节:
str := "hello 世界"
for i, r := range str {
fmt.Printf("index: %d, rune: %c\n", i, r)
}
// '世' 占 3 个字节,index 从 5 开始
如果需要按字节遍历,用传统 for:
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 |
跳转到标签(慎用) |
for i := 0; i < 10; i++ {
if i == 5 {
break // 遇到 5 停止
}
if i%2 == 0 {
continue // 跳过偶数
}
fmt.Println(i) // 只打印奇数
}
[!tip] 💡 带标签的 break 跳出嵌套循环时,可以用标签指定跳出哪个循环:
Outer: for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if i == 1 && j == 1 { break Outer // 直接跳出外层循环 } } }