Go: Go 流程控制:if/switch/for/defer
控制流让程序"会思考"——Go 用极简的 4 个关键字覆盖了所有流程场景,没有
while/do-while的冗余。
Go 的控制流设计哲学:少即是多。if / switch / for / defer 四个关键字,配合 break / continue / label,足够处理 99% 的业务逻辑。这节课你将掌握 Go 流程控制的全部核心。
1. 你将学到
- if 条件判断(含初始化语句)
- switch branch(自动 break,无需手动)
- for 循环的 4 种形式(替代 while/do-while)
- defer 延迟执行与 LIFO 顺序
- break / continue / label 的精准跳转
- 用控制流构建在线考试评分系统
2. 一个在线教育平台的真实故事
(1) 痛点:C++ 评卷代码 500 行还 bug 不断
Charlie 是某在线教育平台的后端工程师,他刚接手了一个自动评卷系统的代码:
"PM 说现在 1,000 份试卷要跑 8 小时,每次出 bug 都是 out of range、C 语言的
if漏了{}导致全年级成绩错乱……"
他打开前任留下的 C++ 代码一看:
// C++ 风格:啰嗦 + 易错
if (score >= 60) {
grade = "及格";
}
else // 注意:C++ else 不在同一行也合法,团队成员风格不统一
grade = "不及格";
// 循环条件漏写 = 死循环
while (true) { // 想写 3 次循环,但忘记写计数器
// ... 500 行后才发现
}
Charlie 接手后,决定用 Go 重写。
(2) Go 的解法
Go 用 4 个关键字覆盖全部流程:
// score_evaluator.go
package main
import "fmt"
func evaluateScore(score int) string {
// (1) if 含初始化语句
if grade := calculateGrade(score); grade != "" {
return fmt.Sprintf("得分 %d,等级 %s", score, grade)
}
return "无效分数"
}
func calculateGrade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 60:
return "C"
default:
return "D"
}
}
func main() {
// (2) for 4 种形式
scores := []int{95, 82, 67, 45, 88}
for i, s := range scores { // range 形式
fmt.Printf("第 %d 题:%d 分 -> %s\n", i+1, s, evaluateScore(s))
}
}
输出:
第 1 题:95 分 -> 得分 95,等级 A
第 2 题:82 分 -> 得分 82,等级 B
第 3 题:67 分 -> 得分 67,等级 C
第 4 题:45 分 -> 得分 45,等级 D
第 5 题:88 分 -> 得分 88,等级 B
(3) 收益:流程控制简洁度对比
| 语言 | if 关键字 | 循环关键字 | switch 自动 break | 评分系统代码量 |
|---|---|---|---|---|
| C/C++ | if |
for/while/do-while |
需要手动 break | ~500 行 |
| Java | if |
for/while/do-while |
需要手动 break | ~400 行 |
| Python | if |
for/while |
N/A(无 switch) | ~350 行 |
| Go | if |
for(4 种形式) |
自动 break | ~200 行 |
for,把 break 从 switch 中默认行为中移除——少 1 个关键字,少 10 类 bug。
3. if 条件判断
(1) 基础 if / else
package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("优秀")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
}
输出:
及格
else 必须和 } 在同一行(强制大括号风格)。}else{ 写法编译错误。
▶ 示例:成绩等级判定
package main
import "fmt"
func judgeGrade(score int) string {
if score >= 90 {
return "A"
} else if score >= 80 {
return "B"
} else if score >= 70 {
return "C"
} else if score >= 60 {
return "D"
} else {
return "F"
}
}
func main() {
fmt.Println(judgeGrade(95)) // A
fmt.Println(judgeGrade(72)) // C
fmt.Println(judgeGrade(50)) // F
}
输出:
A
C
F
(2) if 初始化语句(Go 特色)
if 可以先执行一个初始化语句,再做条件判断——限制变量作用域:
package main
import (
"fmt"
"strconv"
)
func main() {
// 语法:if 初始化语句; 条件 { ... }
// 变量 err 的作用域只在 if-else 块内
if n, err := strconv.Atoi("123"); err == nil {
fmt.Printf("转换成功:%d\n", n)
} else {
fmt.Printf("转换失败:%v\n", err)
}
}
输出:
转换成功:123
▶ 示例:文件读取 + defer 关闭
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 函数返回前自动关闭文件
data := make([]byte, 1024)
n, _ := file.Read(data)
fmt.Printf("读取了 %d 字节\n", n)
return nil
}
func main() {
readFile("config.txt")
}
(3) if 错误处理惯用法(Go 招牌)
Go 的错误处理就是用 if。惯用法:
if err := doSomething(); err != nil {
return err // 早返回
}
// 正常逻辑
error 类型和这个模式的最佳实践。
4. switch 分支(自动 break)
(1) switch 基础:自动 break
Go 的 switch 默认不穿透——匹配到一个 case 后自动退出:
package main
import "fmt"
func main() {
day := "Monday"
switch day {
case "Monday":
fmt.Println("周一:会议日")
case "Tuesday":
fmt.Println("周二:开发日")
case "Friday":
fmt.Println("周五:Demo 日")
default:
fmt.Println("周末:休息")
}
}
输出:
周一:会议日
(2) vs C/Java:无需 break
| 语言 | 写法 | 是否自动 break |
|---|---|---|
| C/C++ | case 1: ...; case 2: ... |
需要手动 break; |
| Java | case 1: ...; case 2: ... |
需要手动 break; |
| Go | case "Monday": ... |
自动 break |
如果想穿透(fall-through),用 fallthrough 关键字:
switch n {
case 1:
fmt.Println("一")
fallthrough // 强制执行下一个 case
case 2:
fmt.Println("二")
}
输出:
一
二
(3) switch 表达式形式(无 tag)
switch 后不跟变量,每个 case 自己写条件:
package main
import "fmt"
func gradeLevel(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 60:
return "C"
default:
return "D"
}
}
func main() {
fmt.Println(gradeLevel(95))
fmt.Println(gradeLevel(72))
}
输出:
A
C
▶ 示例:switch 多值匹配 + HTTP 状态码路由
package main
import "fmt"
func routeStatus(code int) string {
switch code {
case 200, 201, 204:
return "Success"
case 301, 302:
return "Redirect"
case 400, 401, 403, 404:
return "Client Error"
case 500, 502, 503:
return "Server Error"
default:
return "Unknown"
}
}
func main() {
fmt.Println(routeStatus(200)) // Success
fmt.Println(routeStatus(404)) // Client Error
fmt.Println(routeStatus(500)) // Server Error
}
输出:
Success
Client Error
Server Error
5. for 循环的 4 种形式
Go 把所有循环统一为 for,通过不同写法实现 while / do-while / 无限循环。
(1) 经典三段式 for
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
fmt.Printf("第 %d 次\n", i)
}
}
输出:
第 1 次
第 2 次
第 3 次
▶ 示例:while 风格(条件循环)
package main
import "fmt"
func main() {
n := 1
for n < 5 { // 相当于 while (n < 5)
fmt.Printf("n=%d\n", n)
n++
}
}
输出:
n=1
n=2
n=3
n=4
(2) 无限循环
package main
import (
"fmt"
"time"
)
func main() {
// 经典无限循环(实际场景会 break 或 return)
for {
fmt.Println("每秒打印一次...")
time.Sleep(1 * time.Second)
break // 防止真死循环,仅演示用
}
}
输出:
每秒打印一次...
▶ 示例:range 遍历 slice / map / string
package main
import "fmt"
func main() {
scores := []int{95, 82, 67}
// 遍历 slice:index, value
for i, score := range scores {
fmt.Printf("第 %d 题:%d 分\n", i+1, score)
}
// 只要 value:用 _ 忽略 index
for _, score := range scores {
fmt.Printf("得分:%d\n", score)
}
// 遍历 map(key-value)
ages := map[string]int{"Alice": 28, "Bob": 32}
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// 遍历 string(按 rune 而不是 byte)
for i, r := range "Go语言" {
fmt.Printf("位置 %d:%c (码点 %d)\n", i, r, r)
}
}
输出(map 顺序随机):
第 1 题:95 分
第 2 题:82 分
第 3 题:67 分
得分:95
得分:82
得分:67
Alice is 28 years old
Bob is 32 years old
位置 0:G (码点 71)
位置 1:o (码点 111)
位置 2:语 (码点 35821)
位置 4:言 (码点 35328)
range map 的顺序不保证——每次运行可能不同。如果需要稳定顺序,先排序 key 再遍历。
(3) for 四种形式速查
| 形式 | 语法 | 替代 |
|---|---|---|
| 标准 for | for i := 0; i < n; i++ {} |
C/Java for |
| while 风格 | for condition {} |
C/Java while |
| 无限循环 | for {} |
while(true) |
| range 遍历 | for i, v := range slice {} |
Python for...in |
flowchart LR
A[for 初始化; 条件; 后置] --> B{条件为真?}
B -->|是| C[执行循环体]
C --> D[执行后置语句]
D --> B
B -->|否| E[退出循环]
style A stroke-width:2px
style E stroke:#f44,stroke-width:2px
6. defer 延迟执行(LIFO 顺序)
defer 把语句推迟到当前函数返回前执行。常用于资源清理(关闭文件、解锁、关闭连接)。
(1) defer 基础
package main
import "fmt"
func main() {
fmt.Println("1: 函数开始")
defer fmt.Println("3: defer 执行(函数返回前)")
fmt.Println("2: 函数主体")
}
输出:
1: 函数开始
2: 函数主体
3: defer 执行(函数返回前)
(2) defer LIFO 顺序(后进先出)
package main
import "fmt"
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("主逻辑结束")
}
输出:
主逻辑结束
3
2
1
为什么是 LIFO? 因为 defer 模拟"栈式资源管理"——后打开的资源先关闭。
▶ 示例:defer 实战 — 数据库连接释放
package main
import (
"database/sql"
"fmt"
"log"
)
func queryUser(db *sql.DB, userID int) {
// 获取连接
rows, err := db.Query("SELECT name FROM users WHERE id = ?", userID)
if err != nil {
log.Fatal(err)
}
// 关键:defer 释放资源(LIFO 顺序执行)
defer rows.Close()
// 业务逻辑...
for rows.Next() {
var name string
rows.Scan(&name)
fmt.Printf("User: %s\n", name)
}
}
func main() {
fmt.Println("defer 资源管理示例")
// 实际场景:db, _ := sql.Open("mysql", "...")
// queryUser(db, 1)
}
defer file.Close() 即使中途 return 也会执行——这就是 Go 资源管理的招牌用法。
7. break / continue / label 精准跳转
(1) break 跳出循环
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if i == 5 {
break // 跳出整个循环
}
fmt.Printf("%d ", i)
}
fmt.Println("循环结束")
}
输出:
1 2 3 4 循环结束
▶ 示例:continue 跳过 + 累加奇数
package main
import "fmt"
func sumOdd(n int) int {
sum := 0
for i := 1; i <= n; i++ {
if i%2 == 0 {
continue // 跳过偶数
}
sum += i
}
return sum
}
func main() {
fmt.Printf("1~10 奇数和:%d\n", sumOdd(10))
}
输出:
1~10 奇数和:25
(2) label 跳出多层循环
label 给循环起名字,break label 可跳出指定循环:
package main
import "fmt"
func main() {
outer:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if i*j == 6 {
fmt.Printf("命中:i=%d, j=%d\n", i, j)
break outer // 直接跳出外层循环
}
fmt.Printf("(%d, %d) ", i, j)
}
}
fmt.Println("全部结束")
}
输出:
(1, 1) (1, 2) (1, 3) (2, 1) (2, 2) (2, 3) (3, 1) (3, 2) 命中:i=3, j=2
全部结束
(3) 三种跳转对比
| 关键字 | 作用 | 跳出范围 |
|---|---|---|
break |
跳出当前循环或 switch | 单层 |
continue |
跳过本次循环剩余部分 | 单层 |
break label |
跳出指定 label 的循环 | 多层 |
8. 完整示例:在线考试评分系统
把本课所有控制流串起来,实现一个多维度考试评分器:
// exam_evaluator.go
package main
import "fmt"
// 题目结构体
type Question struct {
Type string // single/multi/judge
Correct string // 正确答案
Student string // 学生答案
Score float64 // 分值
}
// 评分函数:用 if 初始化 + switch 表达式形式 + for range
func evaluateExam(name string, questions []Question) (total float64, details []string) {
defer func() {
// defer 收集结果(保证即使中途 return 也能收集)
details = append(details, fmt.Sprintf("[%s] 总分:%.1f", name, total))
}()
for i, q := range questions {
var earned float64
// switch 表达式形式:多条件分支
switch q.Type {
case "single":
if q.Student == q.Correct {
earned = q.Score
}
case "judge":
if q.Student == q.Correct {
earned = q.Score
} else {
earned = 0
}
case "multi":
// 多选题:完全匹配才得分
if q.Student == q.Correct {
earned = q.Score
} else {
earned = 0
}
default:
continue // 跳过未知题型
}
total += earned
details = append(details,
fmt.Sprintf("第 %d 题(%s):得 %.1f 分", i+1, q.Type, earned))
}
return total, details
}
func main() {
questions := []Question{
{Type: "single", Correct: "B", Student: "B", Score: 2},
{Type: "judge", Correct: "true", Student: "true", Score: 1},
{Type: "multi", Correct: "A,B,C", Student: "A,B", Score: 3},
{Type: "single", Correct: "D", Student: "D", Score: 2},
}
// for range 遍历学生名单
students := []string{"Alice", "Bob", "Charlie"}
for _, name := range students {
total, details := evaluateExam(name, questions)
fmt.Println("========================================")
for _, d := range details {
fmt.Println(d)
}
}
}
预期输出(每位学生评分明细 + defer 汇总):
========================================
第 1 题(single):得 2.0 分
第 2 题(judge):得 1.0 分
第 3 题(multi):得 0.0 分
第 4 题(single):得 2.0 分
[Alice] 总分:5.0
========================================
[Bob] 总分:5.0
========================================
[Charlie] 总分:5.0
defer 修改命名返回值才能被外部看到——上面 details 用了命名返回值 (total float64, details []string),defer 里的 append 才能生效。
❓ 常见问题
while 关键字?for 替代所有循环:for condition {} 就是 while,for {} 就是 while(true)。少 1 个关键字,团队不用争论用 while 还是 do-while。fallthrough 显式表达穿透意图,更安全。keys := make([]string, 0, len(m)); for k := range m { keys = append(keys, k) }; sort.Strings(keys)。return 语句执行后、函数真正退出前)。多个 defer 按 LIFO(后进先出)顺序执行,模拟栈式资源管理:后入栈的资源先释放。break + 函数 return 就够了。过度使用 label 会降低代码可读性——goto 时代的教训。switch x := y.(type) 用于接口类型判断,第 7 课会深入。但这种用法只适用于 interface{} 类型变量。break 在 switch 中跳出 switch;在 for 中跳出 for。continue 只能在 for 循环中用,不能用于 switch(switch 不算循环)。📖 小节
- Go 用 4 个关键字覆盖所有流程控制:
if/switch/for/defer if支持初始化语句,限制变量作用域到 if-else 块内switch默认自动 break,需穿透时显式fallthroughfor4 种形式替代 while/do-while:for init;cond;post{}/for cond{}/for{}/for rangedefer在函数返回前执行,多个 defer 按 LIFO(后进先出)顺序break跳出循环、continue跳本次、break label跳多层- Go 的流程控制设计哲学:少关键字 + 自动 break + LIFO 资源管理
📝 作业
-
基础题(难度⭐):用
for+switch写一个 FizzBuzz:打印 1-100,遇到 3 的倍数打印 "Fizz",5 的倍数打印 "Buzz",15 的倍数打印 "FizzBuzz"。 -
进阶题(难度⭐⭐):实现
countPrimes(n int) int函数:返回 1~n 之间的素数个数。要求:(1) 用for循环;(2) 用if判断素数;(3) 用defer在函数返回前打印耗时(用time.Now())。 -
挑战题(难度⭐⭐⭐):实现一个学生成绩分析器:输入一组分数([]float64),输出 (1) 平均分;(2) 及格率(≥60 分);(3) 最高分和最低分;(4) 按等级分组统计(A/B/C/D/F)。要求:(1) 用
for range;(2) 用switch表达式形式判断等级;(3) 用defer在函数返回前打印汇总信息。