控制流
控制流
想象你是一位厨师在做菜:如果锅热了就放油(if),根据食材选择不同的烹饪方式(switch),反复翻炒直到熟透(for),出锅前别忘了关火(defer)。程序也是如此——控制流决定了代码的执行路径,让计算机学会"做决定"和"重复做事"。
1. 核心概念
Go 语言的控制流语句简洁而强大,主要包含以下几类:
1.1 if / else 条件判断
Go 的 if 语句有一个独特之处:可以在条件前添加一个初始化语句,用分号 ; 隔开。变量的作用域仅限于 if/else 块内。
// 基本形式
if 条件 {
// 条件为真时执行
} else if 其他条件 {
// 其他条件为真时执行
} else {
// 以上都不满足时执行
}
// 带初始化语句的形式
if 初始化语句; 条件 {
// 初始化的变量仅在 if/else 块内可见
}
1.2 switch 选择语句
Go 的 switch 与其他语言有两大关键区别:
- 自动 break:每个
case执行完自动终止,不会"贯穿"(fallthrough)到下一个 case。 - 支持类型 switch:可以对变量的类型进行判断。
switch 变量 {
case 值1:
// 处理值1
case 值2, 值3: // 多个值用逗号分隔
// 处理值2或值3
default:
// 默认处理
}
1.3 for 循环
for 是 Go 语言中唯一的循环结构。它取代了其他语言中的 while、do-while 等循环。
| 形式 | 语法 | 用途 |
|---|---|---|
| 传统 for | for init; cond; post {} |
已知循环次数 |
| while 风格 | for cond {} |
条件循环 |
| 无限循环 | for {} |
持续运行 |
| range 遍历 | for i, v := range 集合 {} |
遍历集合 |
1.4 break 和 continue
break:立即跳出当前循环。continue:跳过本次循环剩余代码,进入下一次迭代。
1.5 defer 延迟执行
defer 语句会将函数调用推迟到外层函数返回之后执行。多个 defer 按后进先出(LIFO)的栈顺序执行。
func example() {
defer fmt.Println("第一个注册,最后执行")
defer fmt.Println("第二个注册,倒数第二执行")
fmt.Println("正常执行")
}
// 输出顺序:正常执行 → 第二个注册 → 第一个注册
2. 基本语法/用法
if / else 用法
// 标准 if/else
score := 85
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
if 条件不需要括号,但花括号 {} 是必须的,且 else 必须与 if 的右花括号在同一行。这是 Go 的语法规则,不遵守会导致编译错误。
// 带初始化语句的 if
// err 的作用域仅在 if/else 块内,出了这个块就无法访问
if err := doSomething(); err != nil {
fmt.Println("出错了:", err)
}
if 在 Go 中非常常见,特别是处理错误时。它能让变量的作用域最小化,避免污染外层作用域。
switch 用法
// 基本 switch
day := "周三"
switch day {
case "周一":
fmt.Println("新的一周开始了")
case "周二", "周三", "周四":
fmt.Println("工作日")
case "周五":
fmt.Println("快到周末了")
case "周六", "周日":
fmt.Println("周末愉快")
default:
fmt.Println("无效的日期")
}
switch 不需要在每个 case 后写 break。如果确实需要"贯穿"到下一个 case,可以使用 fallthrough 关键字,但这种用法很少见。
// 无条件 switch(替代长 if-else 链)
score := 85
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
switch 后不跟变量时,它等价于 switch true,可以用来替代冗长的 if-else 链,代码更清晰。
for 循环用法
// 传统 for 循环
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// while 风格
count := 0
for count < 5 {
fmt.Println(count)
count++
}
// 无限循环(需配合 break 使用)
for {
fmt.Println("按 Ctrl+C 退出")
break // 这里用 break 避免真正的无限循环
}
defer 用法
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close() // 确保函数结束时关闭文件
// ... 读取文件内容 ...
}
defer 的参数会在 defer 语句出现时立即求值,而不是在函数执行时求值。注意这个细节,避免意外行为。
示例 1:基础用法(难度⭐)
本示例演示 if/else 和 switch 的基本使用。
package main
import "fmt"
func main() {
// ===== if/else 示例:判断成绩等级 =====
score := 78
// 使用带初始化语句的 if
// rank 变量仅在 if/else 块内有效
if rank := score / 10; rank >= 9 {
fmt.Printf("成绩 %d 分,等级:优秀\n", score)
} else if rank >= 8 {
fmt.Printf("成绩 %d 分,等级:良好\n", score)
} else if rank >= 6 {
fmt.Printf("成绩 %d 分,等级:及格\n", score)
} else {
fmt.Printf("成绩 %d 分,等级:不及格\n", score)
}
// ===== switch 示例:根据月份判断季节 =====
month := 8
switch month {
case 3, 4, 5:
fmt.Printf("%d 月是春季\n", month)
case 6, 7, 8:
fmt.Printf("%d 月是夏季\n", month)
case 9, 10, 11:
fmt.Printf("%d 月是秋季\n", month)
case 12, 1, 2:
fmt.Printf("%d 月是冬季\n", month)
default:
fmt.Printf("%d 不是有效的月份\n", month)
}
// ===== 无条件 switch:替代 if-else 链 =====
hour := 14
switch {
case hour < 6:
fmt.Println("凌晨时分")
case hour < 12:
fmt.Println("上午时光")
case hour < 18:
fmt.Println("下午时光")
default:
fmt.Println("晚上时光")
}
}
输出结果:
成绩 78 分,等级:及格
8 月是夏季
下午时光
代码要点:
rank := score / 10是初始化语句,rank的作用域仅限于if/else块switch的case 3, 4, 5表示匹配 3、4、5 中的任意一个值- 无条件
switch适合替代复杂的if-else链
示例 2:进阶用法(难度⭐⭐)
本示例演示 for 循环的各种形式以及 break / continue 的使用。
package main
import "fmt"
func main() {
// ===== 传统 for 循环:计算 1 到 100 的和 =====
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Printf("1 到 100 的和为: %d\n", sum)
// ===== range 遍历切片 =====
fruits := []string{"苹果", "香蕉", "橘子", "葡萄"}
for index, fruit := range fruits {
fmt.Printf("索引 %d: %s\n", index, fruit)
}
// ===== range 遍历字符串(按 rune) =====
text := "Go语言"
for i, ch := range text {
fmt.Printf("位置 %d: %c (Unicode: %U)\n", i, ch, ch)
}
// ===== break:找到第一个能被 7 整除的数 =====
for i := 1; i <= 100; i++ {
if i%7 == 0 {
fmt.Printf("第一个能被 7 整除的数: %d\n", i)
break // 找到后立即退出循环
}
}
// ===== continue:打印 1-20 中所有奇数 =====
fmt.Print("1-20 的奇数: ")
for i := 1; i <= 20; i++ {
if i%2 == 0 {
continue // 跳过偶数,进入下一次循环
}
fmt.Printf("%d ", i)
}
fmt.Println()
// ===== 带标签的 break:跳出外层循环 =====
fmt.Println("在二维数组中查找数字 5:")
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
found: // 标签名称
for row, cols := range matrix {
for col, val := range cols {
if val == 5 {
fmt.Printf("找到了!位置: 第 %d 行, 第 %d 列\n", row, col)
break found // 跳出外层循环
}
}
}
}
输出结果:
1 到 100 的和为: 5050
索引 0: 苹果
索引 1: 香蕉
索引 2: 橘子
索引 3: 葡萄
位置 0: G (Unicode: U+0047)
位置 1: o (Unicode: U+006F)
位置 2: 语 (Unicode: U+8BED)
位置 5: 言 (Unicode: U+8A00)
第一个能被 7 整除的数: 7
1-20 的奇数: 1 3 5 7 9 11 13 15 17 19
在二维数组中查找数字 5:
找到了!位置: 第 1 行, 第 1 列
代码要点:
for i := 1; i <= 100; i++是标准的三段式循环range遍历切片时返回索引和值,遍历字符串时按 Unicode 字符(rune)迭代break found通过标签跳出指定的外层循环,这在嵌套循环中非常有用
示例 3:综合应用(难度⭐⭐⭐)
本示例综合运用所有控制流语句,实现一个简单的学生成绩管理系统。
package main
import "fmt"
// Student 学生结构体
type Student struct {
Name string
Scores []int
}
// GetAverage 计算平均分
func (s Student) GetAverage() float64 {
if len(s.Scores) == 0 {
return 0
}
total := 0
for _, score := range s.Scores {
total += score
}
return float64(total) / float64(len(s.Scores))
}
// GetGrade 根据平均分获取等级
func (s Student) GetGrade() string {
avg := s.GetAverage()
switch {
case avg >= 90:
return "A (优秀)"
case avg >= 80:
return "B (良好)"
case avg >= 70:
return "C (中等)"
case avg >= 60:
return "D (及格)"
default:
return "F (不及格)"
}
}
// PrintReport 打印成绩报告
func PrintReport(students []Student) {
fmt.Println("========================================")
fmt.Println(" 学 生 成 绩 报 告")
fmt.Println("========================================")
// 使用 defer 确保报告结尾在所有学生数据打印完之后输出
defer fmt.Println("========================================")
defer fmt.Println(" 报 告 生成完毕")
for i, student := range students {
// 使用 defer 演示 LIFO 顺序
defer func(name string, idx int) {
fmt.Printf("[清理] 完成 %s 的报告处理\n", name)
}(student.Name, i)
// 打印学生信息
fmt.Printf("\n学生 %d: %s\n", i+1, student.Name)
fmt.Printf(" 各科成绩: %v\n", student.Scores)
fmt.Printf(" 平均分: %.1f\n", student.GetAverage())
fmt.Printf(" 等级: %s\n", student.GetGrade())
// 分析每科成绩
subjects := []string{"语文", "数学", "英语"}
for j, score := range student.Scores {
// 确保索引不越界
subject := "未知科目"
if j < len(subjects) {
subject = subjects[j]
}
// 对每科成绩进行判断
if score < 60 {
fmt.Printf(" ⚠ %s(%d分) 不及格,需要补考\n", subject, score)
}
}
}
}
// FindTopStudent 查找成绩最好的学生
func FindTopStudent(students []Student) (string, float64) {
if len(students) == 0 {
return "", 0
}
topName := students[0].Name
topAvg := students[0].GetAverage()
for _, s := range students[1:] {
if avg := s.GetAverage(); avg > topAvg {
topName = s.Name
topAvg = avg
}
}
return topName, topAvg
}
func main() {
// 创建学生数据
students := []Student{
{"张三", []int{85, 92, 78}},
{"李四", []int{90, 95, 88}},
{"王五", []int{72, 58, 65}},
{"赵六", []int{95, 98, 92}},
}
// 打印成绩报告
PrintReport(students)
// 查找最优学生
topName, topAvg := FindTopStudent(students)
fmt.Printf("\n🏆 最优学生: %s (平均分: %.1f)\n", topName, topAvg)
}
输出结果:
========================================
学 生 成 绩 报 告
========================================
学生 1: 张三
各科成绩: [85 92 78]
平均分: 85.0
等级: B (良好)
学生 2: 李四
各科成绩: [90 95 88]
平均分: 91.0
等级: A (优秀)
学生 3: 王五
各科成绩: [72 58 65]
平均分: 65.0
等级: D (及格)
⚠ 数学(58分) 不及格,需要补考
学生 4: 赵六
各科成绩: [95 98 92]
平均分: 95.0
等级: A (优秀)
[清理] 完成 赵六 的报告处理
[清理] 完成 王五 的报告处理
[清理] 完成 李四 的报告处理
[清理] 完成 张三 的报告处理
========================================
报 告 生成完毕
========================================
🏆 最优学生: 赵六 (平均分: 95.0)
代码要点:
defer多次注册时按 LIFO(后进先出)顺序执行:赵六 → 王五 → 李四 → 张三defer fmt.Println("报告生成完毕")最先注册却最后执行(因为注册顺序靠前,出栈靠后)switch的无条件形式替代了复杂的if-else链判断等级for range遍历结构体切片,for i, s := range students[1:]从第二个元素开始比较- 匿名函数中的
defer会立即捕获参数值(传入student.Name和i的副本)
3. 常见应用场景
场景 1:错误处理链(if + 初始化语句)
Go 语言中,if 的初始化语句最常见的用途是处理错误:
package main
import (
"fmt"
"strconv"
)
func main() {
// 解析数字并处理错误
inputs := []string{"42", "abc", "100", "xyz", "0"}
for _, input := range inputs {
// 初始化语句中进行类型转换,条件中判断是否出错
if num, err := strconv.Atoi(input); err != nil {
fmt.Printf("❌ 无法解析 %q: %v\n", input, err)
} else {
fmt.Printf("✅ 解析成功: %q → %d\n", input, num)
}
}
}
场景 2:遍历 Map 并条件筛选(for + switch)
package main
import "fmt"
func main() {
// 学生成绩表
students := map[string]int{
"张三": 85,
"李四": 92,
"王五": 58,
"赵六": 76,
"钱七": 45,
}
// 统计各等级人数
excellent, good, pass, fail := 0, 0, 0, 0
for name, score := range students {
switch {
case score >= 90:
excellent++
fmt.Printf("⭐ %s: %d (优秀)\n", name, score)
case score >= 80:
good++
fmt.Printf("👍 %s: %d (良好)\n", name, score)
case score >= 60:
pass++
fmt.Printf("✅ %s: %d (及格)\n", name, score)
default:
fail++
fmt.Printf("❌ %s: %d (不及格)\n", name, score)
}
}
fmt.Printf("\n统计: 优秀 %d 人, 良好 %d 人, 及格 %d 人, 不及格 %d 人\n",
excellent, good, pass, fail)
}
❓ 常见问题
Q1:为什么 Go 的 switch 不需要 break?
Go 语言设计者认为 C 语言中 switch 的 fallthrough(贯穿)行为是导致 bug 的常见来源。Go 的 switch 默认每个 case 执行完就自动退出,避免了忘记写 break 导致的意外贯穿。如果确实需要贯穿,可以显式使用 fallthrough 关键字。
// fallthrough 的用法(少见)
x := 1
switch x {
case 1:
fmt.Println("一")
fallthrough // 继续执行下一个 case 的代码
case 2:
fmt.Println("二") // 会被执行
default:
fmt.Println("其他")
}
// 输出: 一 \n 二
Q2:for range 遍历时变量是复制的吗?
是的。for range 中的值变量是元素的副本,修改它不会影响原集合。如果需要修改原集合中的元素,应通过索引访问。
nums := []int{1, 2, 3, 4, 5}
// ❌ 错误:修改的是副本
for _, n := range nums {
n *= 2 // 不会影响 nums
}
// ✅ 正确:通过索引修改原切片
for i := range nums {
nums[i] *= 2 // 会修改 nums
}
Q3:defer 的参数什么时候求值?
defer 的参数在 defer 语句出现时立即求值,而不是在函数返回时求值。
func main() {
x := 10
defer fmt.Println("defer 中的 x:", x) // x 在此处求值为 10
x = 20
fmt.Println("正常执行的 x:", x)
}
// 输出:
// 正常执行的 x: 20
// defer 中的 x: 10 (不是 20!)
Q4:如何在循环中使用 goroutine 时正确捕获循环变量?
在 Go 1.22 之前,for 循环变量在所有迭代中共享同一个地址,容易在 goroutine 中产生问题。Go 1.22+ 已修复此行为,每次迭代都有独立的变量。如果使用旧版本,需要将变量作为参数传入:
// Go 1.22+ 直接使用即可
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 每个 goroutine 拿到独立的 i
}()
}
// 旧版本需要显式传参
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n) // n 是 i 的副本
}(i)
}
📖 小节
if/else支持初始化语句(if init; cond {}),变量作用域限于块内switch默认不需要 break,每个 case 自动终止;支持无条件 switch 替代 if-else 链for是 Go 唯一的循环,有四种形式:传统 for、while 风格、无限循环、range 遍历break可配合标签跳出指定循环层,continue跳过当前迭代defer按后进先出(LIFO)顺序执行,参数在 defer 语句处立即求值range遍历返回的是副本,修改副本不会影响原集合
📝 作业
练习 1(⭐)
编写一个程序,输入一个年份,判断是否为闰年。规则:
- 能被 4 整除但不能被 100 整除,或者能被 400 整除的是闰年。
- 使用
if/else的初始化语句形式。
package main
import "fmt"
func main() {
year := 2024 // 可以修改这个值测试
// 在这里编写你的代码
// 提示:使用 if init; cond 形式
}
练习 2(⭐⭐)
编写一个程序,使用 for 循环打印九九乘法表。格式要求:
- 每行一个乘数
- 使用制表符
\t对齐 - 综合使用
for循环和格式化输出
package main
import "fmt"
func main() {
// 在这里编写你的代码
// 提示:使用两层嵌套 for 循环
// 外层循环 i 从 1 到 9
// 内层循环 j 从 1 到 i
}
练习 3(⭐⭐⭐)
编写一个程序,实现一个简单的猜数字游戏:
- 使用循环让用户反复猜测,直到猜对为止
- 每次猜测后给出"太大"或"太小"的提示
- 使用
defer记录游戏开始和结束的时间 - 使用
switch根据猜测次数给出不同评价(1-3 次:天才,4-6 次:不错,7+ 次:继续努力)
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// 使用 defer 记录结束时间
start := time.Now()
defer func() {
fmt.Printf("\n游戏用时: %v\n", time.Since(start))
}()
// 生成 1-100 的随机数
target := rand.Intn(100) + 1
attempts := 0
fmt.Println("=== 猜数字游戏 ===")
fmt.Println("我想了一个 1-100 之间的数字,来猜猜看!")
// 在这里编写你的代码
// 提示:
// 1. 使用 for 循环持续游戏
// 2. 使用 fmt.Scan 读取用户输入
// 3. 使用 switch 判断猜测结果和评价等级
// 4. 猜对时使用 break 退出循环
}
下一课
下一课:函数 → 学习 Go 语言的函数定义、多返回值、可变参数、匿名函数和闭包等核心概念。



