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

5.0 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go基础语法
循环
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 的 for
  • for cond {} ≈ C 的 while
  • for {} ≈ 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  // 直接跳出外层循环
        }
    }
}