Go 入门到精通-07-流程控制之循环结构

目录

🟢 Go 入门到精通 - 流程控制之循环结构

📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处 | 🧑‍💻 作者:布朗克168



一、Go的循环哲学:一个for打天下

如果说Go在条件判断上还有ifswitch两个关键字,那么在循环领域,Go做出了最激进的设计:

🎯 Go只有一种循环结构------for。没有while,没有do-while,也没有for-each

复制代码
┌──────────────────────────────────────────────────────────┐
│                 Go for 循环 = 万能循环                     │
├────────────────┬──────────────────┬──────────────────────┤
│  传统C风格      │   while风格        │   无限循环            │
│                │                  │                      │
│  for i:=0;     │  for condition { │  for {               │
│    i<n; i++ {  │    ...           │    ...               │
│    ...         │  }               │  }                   │
│  }             │                  │                      │
├────────────────┴──────────────────┴──────────────────────┤
│                    + range 迭代器                         │
│            for i, v := range collection { ... }          │
└──────────────────────────────────────────────────────────┘

三种形态+range=涵盖了所有循环场景。这正是Go"少即是多"哲学的完美体现。


二、for循环的三种形态

2.1 传统C风格for

最完整的for循环形式,由三部分组成:

go 复制代码
for 初始化语句; 条件表达式; 后置语句 {
    // 循环体
}

执行顺序:初始化 → 条件判断 → 循环体 → 后置语句 → 条件判断 → ...

go 复制代码
package main

import "fmt"

func main() {
    // 示例1:打印1到5
    for i := 1; i <= 5; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 输出:1 2 3 4 5

    // 示例2:计算1到100的和
    sum := 0
    for i := 1; i <= 100; i++ {
        sum += i
    }
    fmt.Println("1到100的和:", sum) // 5050

    // 示例3:倒序循环
    for i := 10; i >= 1; i-- {
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 输出:10 9 8 7 6 5 4 3 2 1

    // 示例4:步长为2
    for i := 0; i <= 20; i += 2 {
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 输出:0 2 4 6 8 10 12 14 16 18 20

    // 示例5:多个变量(使用平行赋值)
    for i, j := 0, 10; i < j; i, j = i+1, j-1 {
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

⚠️ 注意 :Go的++--语句不是表达式 ,所以不能写for i := 0; i < n; i = i++,也不能嵌套在表达式中使用。

2.2 while式for(条件循环)

去掉初始化语句和后置语句,就是while的等价物:

go 复制代码
package main

import "fmt"

func main() {
    // 这是Go中的while循环
    i := 0
    for i < 5 { // 等价于 while (i < 5)
        fmt.Print(i, " ")
        i++
    }
    fmt.Println()
    // 输出:0 1 2 3 4

    // 实战:二分查找
    func binarySearch(arr []int, target int) int {
        left, right := 0, len(arr)-1
        for left <= right {
            mid := left + (right-left)/2
            if arr[mid] == target {
                return mid
            } else if arr[mid] < target {
                left = mid + 1
            } else {
                right = mid - 1
            }
        }
        return -1
    }
}

for condition {} 是Go社区最推荐的"while"写法,语义清晰。

2.3 无限循环for

最简洁的循环形式------连条件都省了:

go 复制代码
// Go的无限循环:for {}
// 等价于其他语言的 while (true) 或 for (;;)

for {
    // 需要显式break跳出
    if someCondition {
        break
    }
}
go 复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    // 示例:服务器主循环
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    counter := 0
    for {
        select {
        case t := <-ticker.C:
            fmt.Println("心跳:", t.Format("15:04:05"))
            counter++
            if counter >= 5 {
                fmt.Println("5次心跳后退出")
                return // 退出整个函数
            }
        }
    }

    // 或者更简单的计数器
    count := 0
    for {
        count++
        fmt.Println("第", count, "次循环")
        if count >= 3 {
            break // 跳出循环
        }
    }
}

🎯 for {} 是Go服务器程序中常见的模式------配合select和channel,构建事件驱动的无限循环。


三、range遍历:Go的迭代利器

range关键字是Go中最常用的迭代方式,它可以遍历几乎所有集合类型。

3.1 遍历数组/切片

go 复制代码
package main

import "fmt"

func main() {
    fruits := []string{"🍎", "🍌", "🍊", "🍇", "🍓"}

    // 同时获取索引和值
    for i, fruit := range fruits {
        fmt.Printf("fruits[%d] = %s\n", i, fruit)
    }

    // 只要索引
    for i := range fruits {
        fmt.Printf("索引: %d\n", i)
    }

    // 只要值(用_忽略索引)
    for _, fruit := range fruits {
        fmt.Printf("水果: %s\n", fruit)
    }

    // 仅利用循环次数
    for range fruits {
        fmt.Println("发现一个水果!")
    }
}

输出:

复制代码
fruits[0] = 🍎
fruits[1] = 🍌
fruits[2] = 🍊
fruits[3] = 🍇
fruits[4] = 🍓

3.2 遍历Map

go 复制代码
scores := map[string]int{
    "张三": 95,
    "李四": 88,
    "王五": 72,
}

// 遍历所有键值对(顺序是随机的!)
for name, score := range scores {
    fmt.Printf("%s: %d分\n", name, score)
}

// 只要键
for name := range scores {
    fmt.Println("学生:", name)
}

// 只要值
for _, score := range scores {
    fmt.Println("分数:", score)
}

⚠️ 重要提醒 :Map的遍历顺序是随机的(Go运行时故意打乱顺序),不要依赖遍历顺序!

3.3 遍历字符串

go 复制代码
// range遍历字符串会按Unicode码点(rune)处理
str := "Hello中国"

for i, ch := range str {
    fmt.Printf("索引%d: %c (Unicode: %U)\n", i, ch, ch)
}

// 输出:
// 索引0: H (Unicode: U+0048)
// 索引1: e (Unicode: U+0065)
// 索引2: l (Unicode: U+006C)
// 索引3: l (Unicode: U+006C)
// 索引4: o (Unicode: U+006F)
// 索引5: 中 (Unicode: U+4E2D)  ← 注意索引跳过了6
// 索引8: 国 (Unicode: U+56FD)  ← 一个汉字占3个字节

关键对比

遍历方式 迭代单元 中文处理
for i := 0; i < len(s); i++ 字节(byte) 一个汉字=3个字节,会被拆开
for i, ch := range s Unicode字符(rune) 正确处理多字节字符

3.4 遍历Channel

go 复制代码
package main

import "fmt"

func main() {
    ch := make(chan int, 5)

    // 发送数据
    go func() {
        for i := 1; i <= 5; i++ {
            ch <- i
        }
        close(ch) // 必须关闭,否则range会死锁
    }()

    // range接收,直到channel关闭
    for v := range ch {
        fmt.Println("收到:", v)
    }
    // 输出:
    // 收到: 1
    // 收到: 2
    // 收到: 3
    // 收到: 4
    // 收到: 5
}

3.5 range返回值详解

数据类型 第一个返回值 第二个返回值 备注
[]T / [N]T 索引(int) 元素值(T) 值类型是副本
map[K]V 键(K) 值(V) 遍历顺序随机
string 字节索引(int) rune(rune) 正确处理UTF-8
chan T 元素值(T) 直到channel关闭

四、循环控制:break与continue

4.1 基础break/continue

go 复制代码
package main

import "fmt"

func main() {
    // break:立即终止当前循环
    fmt.Println("=== break 演示 ===")
    for i := 1; i <= 10; i++ {
        if i == 5 {
            break // 到5就停
        }
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 输出:1 2 3 4

    // continue:跳过本次迭代,进入下一次
    fmt.Println("=== continue 演示 ===")
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            continue // 跳过偶数
        }
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 输出:1 3 5 7 9
}

4.2 标签(Label)跳转

当遇到嵌套循环 时,普通的break只能跳出当前层循环。标签(Label)允许你跳出任意外层循环:

go 复制代码
package main

import "fmt"

func main() {
    // 场景:在二维数组中查找目标值,找到后完全退出
    matrix := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    target := 5

    fmt.Println("=== 标签break ===")
    OuterLoop:
    for i, row := range matrix {
        for j, val := range row {
            fmt.Printf("检查 matrix[%d][%d]=%d\n", i, j, val)
            if val == target {
                fmt.Printf("✅ 找到 %d 位于 matrix[%d][%d]\n", target, i, j)
                break OuterLoop // 跳出外层循环
            }
        }
    }
    fmt.Println("搜索结束")

    // 标签continue:跳过外层循环的当前迭代
    fmt.Println("\n=== 标签continue ===")
    OuterFor:
    for i := 1; i <= 3; i++ {
        for j := 1; j <= 3; j++ {
            if i == 2 && j == 2 {
                fmt.Printf("跳过 i=%d 的剩余迭代\n", i)
                continue OuterFor // 跳到外层循环的下一次迭代
            }
            fmt.Printf("i=%d, j=%d\n", i, j)
        }
    }
}

输出:

复制代码
=== 标签break ===
检查 matrix[0][0]=1
检查 matrix[0][1]=2
检查 matrix[0][2]=3
检查 matrix[1][0]=4
检查 matrix[1][1]=5
✅ 找到 5 位于 matrix[1][1]
搜索结束

=== 标签continue ===
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
跳过 i=2 的剩余迭代
i=3, j=1
i=3, j=2
i=3, j=3

📌 标签命名规范 :Go社区习惯使用驼峰命名,如OuterLoop,标签定义在for语句前。


五、goto语句:争议中的存在

goto是编程语言中最具争议的关键字------Dijkstra早在1968年就发表了著名的《Go To Statement Considered Harmful》。但Go仍然保留了goto,只是限制了使用场景。

go 复制代码
package main

import "fmt"

func main() {
    i := 0

    // goto的基本语法
    Loop:           // 标签定义
    fmt.Println(i)
    i++
    if i < 3 {
        goto Loop  // 跳转到Loop标签
    }

    // 输出:
    // 0
    // 1
    // 2
}

goto的使用限制

Go对goto做出了严格限制,避免经典的"意大利面条代码":

限制 说明
✅ 同函数内跳转 不能跨函数
❌ 不能跳过变量声明 跳转后不能出现未定义的变量
❌ 不能跳入代码块 不能从外部跳入if/for/swtich内部
✅ 可以跳出代码块 这是少数合理使用场景
go 复制代码
// ❌ 编译错误:goto跳过了变量声明
goto Label
x := 10 // 这行被跳过了
Label:
fmt.Println(x)

// ❌ 编译错误:goto跳入代码块内部
goto Inner
if true {
Inner:
    fmt.Println("内部")
}

// ✅ 合法的goto:跳出嵌套结构
func cleanup() {
    // 统一错误处理模式
    if err := step1(); err != nil {
        goto HandleError
    }
    if err := step2(); err != nil {
        goto HandleError
    }
    if err := step3(); err != nil {
        goto HandleError
    }
    fmt.Println("所有步骤成功")
    return

HandleError:
    fmt.Println("发生错误,执行清理...")
    // 清理资源
}

💡 实用建议 :goto在Go标准库中偶尔用于错误处理和状态机,但绝大多数场景用break+标签或早返回更清晰。如果你的代码需要goto,先停下来思考是否设计有问题。


六、循环嵌套实战

实战1:打印九九乘法表

go 复制代码
package main

import "fmt"

func main() {
    fmt.Println("📐 九九乘法表")
    fmt.Println("============")

    for i := 1; i <= 9; i++ {
        for j := 1; j <= i; j++ {
            fmt.Printf("%d×%d=%-2d ", j, i, i*j)
        }
        fmt.Println()
    }
}

输出:

复制代码
📐 九九乘法表
============
1×1=1
1×2=2  2×2=4
1×3=3  2×3=6  3×3=9
1×4=4  2×4=8  3×4=12 4×4=16
...

实战2:找出100以内的所有素数

go 复制代码
package main

import (
    "fmt"
    "math"
)

func isPrime(n int) bool {
    if n < 2 {
        return false
    }
    for i := 2; i <= int(math.Sqrt(float64(n))); i++ {
        if n%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Println("🔢 100以内的素数:")
    count := 0
    for i := 2; i <= 100; i++ {
        if isPrime(i) {
            fmt.Printf("%3d ", i)
            count++
            if count%10 == 0 {
                fmt.Println()
            }
        }
    }
    fmt.Printf("\n共 %d 个素数\n", count)
}

实战3:棋盘格生成器

go 复制代码
package main

import "fmt"

func main() {
    size := 8
    for row := 0; row < size; row++ {
        for col := 0; col < size; col++ {
            if (row+col)%2 == 0 {
                fmt.Print("⬜")
            } else {
                fmt.Print("⬛")
            }
        }
        fmt.Println()
    }
}

七、Go vs Java 循环对比

特性 Go Java
循环关键字 只有for一种 for, while, do-while, for-each
传统for for i:=0; i<n; i++ {} for (int i=0; i<n; i++) {}
while循环 for condition {} while (condition) {}
无限循环 for {} while(true) {}for(;;){}
集合遍历 for i, v := range col {} for (Type v : col) {}
++/--位置 只能是后置语句 可在表达式中
标签跳转 break Label break Label
goto ✅ 有限制 ❌ 保留关键字但不使用
遍历map 随机顺序 取决于实现

八、常见陷阱与最佳实践

陷阱1:range循环中的变量复用

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 (不是 0 1 2)

// ✅ 解决方案:在循环内创建局部变量
for i := 0; i < 3; i++ {
    i := i // 创建新的局部变量
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}
// 输出:0 1 2 ✅

// Go 1.22+ 已经修复了这个问题
// for i := 0; i < 3; i++ 中的i每次迭代都是新变量

陷阱2:range返回的是副本

go 复制代码
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"张三", 20},
    {"李四", 25},
}

// ❌ 修改无效:v是副本
for _, v := range people {
    v.Age++ // 只修改了副本,原始数据不变
}
fmt.Println(people) // [{张三 20} {李四 25}] 没变!

// ✅ 使用索引修改
for i := range people {
    people[i].Age++ // 直接修改切片中的元素
}
fmt.Println(people) // [{张三 21} {李四 26}]

陷阱3:遍历时修改切片长度

go 复制代码
// ⚠️ 危险:在range遍历中append可能导致死循环或panic
items := []int{1, 2, 3}
for i, v := range items {
    fmt.Println(i, v)
    items = append(items, v*2) // 不断增长
}
// 输出结果不确定,但不会无限循环(range在开始时确定长度)

// ✅ 安全做法:使用传统for循环
for i := 0; i < len(items); i++ {
    items = append(items, items[i]*2)
    if len(items) > 100 {
        break // 设置上限
    }
}

最佳实践总结

场景 推荐写法 原因
遍历全部元素 for _, v := range s 简洁清晰
需要索引 for i, v := range s 同时获取索引和值
需要修改元素 for i := range s + s[i] = ... 避免副本陷阱
条件循环 for condition {} 比while语义更Go风格
服务器主循环 for { select {...} } Go并发编程标配
跳出嵌套循环 break Label 清晰高效

九、小结与预告

📝 核心知识点回顾

知识点 核心内容
三种for形态 传统/while式/无限循环,一个关键字覆盖所有
range迭代 遍历数组/切片/map/string/channel,注意返回副本
break/continue 基础用法+标签跳转,标签用于嵌套循环跳出
goto 有限制使用,主要用于错误处理统一出口
嵌套循环 九九乘法表、素数、矩阵遍历等经典场景
常见陷阱 闭包变量复用、range副本、遍历中修改集合

🤔 互动问题

  1. 你更习惯Go的for condition {}还是传统语言的while?为什么Go不保留while关键字?
  2. 在什么场景下你会使用标签break?它比提取函数的做法好在哪里?
  3. Go 1.22引入了循环变量语义变更------你认为这是改进还是破坏向后兼容?

📖 下篇预告

下一篇将进入Go复合类型的核心------数组与切片。切片是Go中最常用的数据结构,它的底层结构(ptr/len/cap)、扩容机制和常见陷阱,是每个Go开发者必须深入理解的内容。我们还将对比数组和切片的差异,帮你彻底避免"子切片共享底层数组"的坑!


📚 参考资料


💡 学习建议:循环是编程的基本功,建议手写九九乘法表、冒泡排序、斐波那契数列等经典习题。特别注意range返回副本的陷阱------这是Go面试中的高频考点。掌握标签break/continue可以在复杂嵌套循环场景中写出更优雅的代码。