Go: Go 流程控制:if/switch/for/defer

控制流让程序"会思考"——Go 用极简的 4 个关键字覆盖了所有流程场景,没有 while / do-while 的冗余。

Go 的控制流设计哲学:少即是多if / switch / for / defer 四个关键字,配合 break / continue / label,足够处理 99% 的业务逻辑。这节课你将掌握 Go 流程控制的全部核心。

1. 你将学到


2. 一个在线教育平台的真实故事

(1) 痛点:C++ 评卷代码 500 行还 bug 不断

Charlie 是某在线教育平台的后端工程师,他刚接手了一个自动评卷系统的代码:

"PM 说现在 1,000 份试卷要跑 8 小时,每次出 bug 都是 out of range、C 语言的 if 漏了 {} 导致全年级成绩错乱……"

他打开前任留下的 C++ 代码一看:

CPP
// C++ 风格:啰嗦 + 易错
if (score >= 60) {
    grade = "及格";
}
else  // 注意:C++ else 不在同一行也合法,团队成员风格不统一
    grade = "不及格";

// 循环条件漏写 = 死循环
while (true) {  // 想写 3 次循环,但忘记写计数器
    // ... 500 行后才发现
}

Charlie 接手后,决定用 Go 重写。

(2) Go 的解法

Go 用 4 个关键字覆盖全部流程:

GO
// 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))
    }
}

输出:

TEXT 📖 仅展示
第 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 行
💡 提示: Go 把所有循环统一为 for,把 breakswitch 中默认行为中移除——少 1 个关键字,少 10 类 bug。


3. if 条件判断

(1) 基础 if / else

GO
package main

import "fmt"

func main() {
    score := 85

    if score >= 90 {
        fmt.Println("优秀")
    } else if score >= 60 {
        fmt.Println("及格")
    } else {
        fmt.Println("不及格")
    }
}

输出:

TEXT 📖 仅展示
及格
🔥 易错: Go 的 else 必须和 } 在同一行(强制大括号风格)。}else{ 写法编译错误。

▶ 示例:成绩等级判定

GO
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
}
▶ 试一试

输出:

TEXT 📖 仅展示
A
C
F

(2) if 初始化语句(Go 特色)

if 可以先执行一个初始化语句,再做条件判断——限制变量作用域:

GO
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)
    }
}

输出:

TEXT 📖 仅展示
转换成功:123

▶ 示例:文件读取 + defer 关闭

GO
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。惯用法:

GO
if err := doSomething(); err != nil {
    return err  // 早返回
}
// 正常逻辑
💡 提示: 第 8 课会深入讲解 error 类型和这个模式的最佳实践。


4. switch 分支(自动 break)

(1) switch 基础:自动 break

Go 的 switch 默认不穿透——匹配到一个 case 后自动退出:

GO
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("周末:休息")
    }
}

输出:

TEXT 📖 仅展示
周一:会议日

(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 关键字:

GO
switch n {
case 1:
    fmt.Println("一")
    fallthrough  // 强制执行下一个 case
case 2:
    fmt.Println("二")
}

输出:

TEXT 📖 仅展示
一
二

(3) switch 表达式形式(无 tag)

switch 后不跟变量,每个 case 自己写条件:

GO
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))
}

输出:

TEXT 📖 仅展示
A
C

▶ 示例:switch 多值匹配 + HTTP 状态码路由

GO
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
}
▶ 试一试

输出:

TEXT 📖 仅展示
Success
Client Error
Server Error

5. for 循环的 4 种形式

Go 把所有循环统一为 for,通过不同写法实现 while / do-while / 无限循环。

(1) 经典三段式 for

GO
package main

import "fmt"

func main() {
    for i := 1; i <= 3; i++ {
        fmt.Printf("第 %d 次\n", i)
    }
}

输出:

TEXT 📖 仅展示
第 1 次
第 2 次
第 3 次

▶ 示例:while 风格(条件循环)

GO
package main

import "fmt"

func main() {
    n := 1
    for n < 5 {  // 相当于 while (n < 5)
        fmt.Printf("n=%d\n", n)
        n++
    }
}
▶ 试一试

输出:

TEXT 📖 仅展示
n=1
n=2
n=3
n=4

(2) 无限循环

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // 经典无限循环(实际场景会 break 或 return)
    for {
        fmt.Println("每秒打印一次...")
        time.Sleep(1 * time.Second)
        break  // 防止真死循环,仅演示用
    }
}

输出:

TEXT 📖 仅展示
每秒打印一次...

▶ 示例:range 遍历 slice / map / string

GO
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 顺序随机):

TEXT 📖 仅展示
第 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
100%
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 基础

GO
package main

import "fmt"

func main() {
    fmt.Println("1: 函数开始")

    defer fmt.Println("3: defer 执行(函数返回前)")

    fmt.Println("2: 函数主体")
}

输出:

TEXT 📖 仅展示
1: 函数开始
2: 函数主体
3: defer 执行(函数返回前)

(2) defer LIFO 顺序(后进先出)

GO
package main

import "fmt"

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")

    fmt.Println("主逻辑结束")
}

输出:

TEXT 📖 仅展示
主逻辑结束
3
2
1

为什么是 LIFO? 因为 defer 模拟"栈式资源管理"——后打开的资源先关闭。

▶ 示例:defer 实战 — 数据库连接释放

GO
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 跳出循环

GO
package main

import "fmt"

func main() {
    for i := 1; i <= 10; i++ {
        if i == 5 {
            break  // 跳出整个循环
        }
        fmt.Printf("%d ", i)
    }
    fmt.Println("循环结束")
}

输出:

TEXT 📖 仅展示
1 2 3 4 循环结束

▶ 示例:continue 跳过 + 累加奇数

GO
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))
}
▶ 试一试

输出:

TEXT 📖 仅展示
1~10 奇数和:25

(2) label 跳出多层循环

label 给循环起名字,break label 可跳出指定循环:

GO
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("全部结束")
}

输出:

TEXT 📖 仅展示
(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. 完整示例:在线考试评分系统

把本课所有控制流串起来,实现一个多维度考试评分器:

GO
// 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 汇总):

TEXT 📖 仅展示
========================================
第 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 才能生效。


❓ 常见问题

Q Go 为什么没有 while 关键字?
A Go 用 for 替代所有循环:for condition {} 就是 while,for {} 就是 while(true)。少 1 个关键字,团队不用争论用 while 还是 do-while
Q switch 默认不穿透,会不会很麻烦?
A 恰恰相反,Go 团队认为 90% 的 switch case 都应该独立——C/Java 的"忘写 break 导致 fall-through"是经典 bug 之一。Go 用 fallthrough 显式表达穿透意图,更安全。
Q for range 遍历 map 顺序为什么乱?
A Go 故意设计为乱序——避免程序员依赖 map 顺序。如果需要稳定顺序,先把 key 排序再遍历:keys := make([]string, 0, len(m)); for k := range m { keys = append(keys, k) }; sort.Strings(keys)
Q defer 什么时候执行?多个 defer 什么顺序?
A defer 在当前函数返回前执行(即 return 语句执行后、函数真正退出前)。多个 defer 按 LIFO(后进先出)顺序执行,模拟栈式资源管理:后入栈的资源先释放。
Q if 初始化语句的作用域?
A 初始化语句声明的变量只在 if-else 块内可见,出了 if-else 就失效。这避免了"变量泄漏到外层作用域"的常见 bug。
Q label 在实际项目里用得多吗?
A 很少见。label 主要用于跳出多层循环的场景,99% 的业务用 break + 函数 return 就够了。过度使用 label 会降低代码可读性——goto 时代的教训。
Q Go 的 switch 支持类型判断吗?
A 支持。switch x := y.(type) 用于接口类型判断,第 7 课会深入。但这种用法只适用于 interface{} 类型变量。
Q continue 和 break 在 switch 里能用吗?
A 能,但行为不同。break 在 switch 中跳出 switch;在 for 中跳出 for。continue 只能在 for 循环中用,不能用于 switch(switch 不算循环)。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 for + switch 写一个 FizzBuzz:打印 1-100,遇到 3 的倍数打印 "Fizz",5 的倍数打印 "Buzz",15 的倍数打印 "FizzBuzz"。

  2. 进阶题(难度⭐⭐):实现 countPrimes(n int) int 函数:返回 1~n 之间的素数个数。要求:(1) 用 for 循环;(2) 用 if 判断素数;(3) 用 defer 在函数返回前打印耗时(用 time.Now())。

  3. 挑战题(难度⭐⭐⭐):实现一个学生成绩分析器:输入一组分数([]float64),输出 (1) 平均分;(2) 及格率(≥60 分);(3) 最高分和最低分;(4) 按等级分组统计(A/B/C/D/F)。要求:(1) 用 for range;(2) 用 switch 表达式形式判断等级;(3) 用 defer 在函数返回前打印汇总信息。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏