目录
- [🟢 Go 入门到精通 - 流程控制之循环结构](#🟢 Go 入门到精通 - 流程控制之循环结构)
-
- 一、Go的循环哲学:一个for打天下
- 二、for循环的三种形态
-
- [2.1 传统C风格for](#2.1 传统C风格for)
- [2.2 while式for(条件循环)](#2.2 while式for(条件循环))
- [2.3 无限循环for](#2.3 无限循环for)
- 三、range遍历:Go的迭代利器
-
- [3.1 遍历数组/切片](#3.1 遍历数组/切片)
- [3.2 遍历Map](#3.2 遍历Map)
- [3.3 遍历字符串](#3.3 遍历字符串)
- [3.4 遍历Channel](#3.4 遍历Channel)
- [3.5 range返回值详解](#3.5 range返回值详解)
- 四、循环控制:break与continue
-
- [4.1 基础break/continue](#4.1 基础break/continue)
- [4.2 标签(Label)跳转](#4.2 标签(Label)跳转)
- 五、goto语句:争议中的存在
- 六、循环嵌套实战
- [七、Go vs Java 循环对比](#七、Go vs Java 循环对比)
- 八、常见陷阱与最佳实践
- 九、小结与预告
-
- [📝 核心知识点回顾](#📝 核心知识点回顾)
- [🤔 互动问题](#🤔 互动问题)
- [📖 下篇预告](#📖 下篇预告)
- [📚 参考资料](#📚 参考资料)
🟢 Go 入门到精通 - 流程控制之循环结构
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处 | 🧑💻 作者:布朗克168
一、Go的循环哲学:一个for打天下
如果说Go在条件判断上还有if和switch两个关键字,那么在循环领域,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副本、遍历中修改集合 |
🤔 互动问题
- 你更习惯Go的
for condition {}还是传统语言的while?为什么Go不保留while关键字? - 在什么场景下你会使用标签break?它比提取函数的做法好在哪里?
- Go 1.22引入了循环变量语义变更------你认为这是改进还是破坏向后兼容?
📖 下篇预告
下一篇将进入Go复合类型的核心------数组与切片。切片是Go中最常用的数据结构,它的底层结构(ptr/len/cap)、扩容机制和常见陷阱,是每个Go开发者必须深入理解的内容。我们还将对比数组和切片的差异,帮你彻底避免"子切片共享底层数组"的坑!
📚 参考资料
- Go官方文档 - For语句
- Go语言规范 - For statements
- Go Blog - Go 1.22循环变量变更
- Dijkstra - Go To Statement Considered Harmful (1968)
💡 学习建议:循环是编程的基本功,建议手写九九乘法表、冒泡排序、斐波那契数列等经典习题。特别注意range返回副本的陷阱------这是Go面试中的高频考点。掌握标签break/continue可以在复杂嵌套循环场景中写出更优雅的代码。