|
|
@@ -0,0 +1,172 @@
|
|
|
+# 第 12 课:switch 与 defer
|
|
|
+
|
|
|
+## 学习目标
|
|
|
+
|
|
|
+学习两个独立的控制流程工具:switch 用于多分支选择,defer 用于安排当前函数返回前的操作。先掌握各自用途,不要把它们理解成必须组合使用的语法。
|
|
|
+
|
|
|
+按 [main.go](./main.go) 逐段手敲。本课只用标准库 fmt,不需要新增依赖;暂不引入匿名函数、文件操作、panic / recover 或命名返回值。
|
|
|
+
|
|
|
+## 1. switch 按值选择
|
|
|
+
|
|
|
+```go
|
|
|
+switch command {
|
|
|
+case "build":
|
|
|
+ fmt.Println("编译")
|
|
|
+case "run":
|
|
|
+ fmt.Println("编译并运行")
|
|
|
+default:
|
|
|
+ fmt.Println("未知指令")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 计算 switch 后面的值,找到匹配的 case 并执行。
|
|
|
+- 没有匹配项时执行 default;default 可以省略,省略后没匹配就不执行分支。
|
|
|
+- case 后面用冒号,大括号包住整个 switch。
|
|
|
+- 与 C 不同,Go 默认不会执行后面的 case,通常不需要每个 case 都写 break。
|
|
|
+- 多个值可以共用分支,例如 `case 6, 7:`。
|
|
|
+
|
|
|
+Go 也有显式的 fallthrough,但会无条件进入下一分支,不是重新判断下一条件;本课不使用它,先掌握默认的不贯穿行为。
|
|
|
+
|
|
|
+switch 中的 break 默认结束 switch,不一定结束外层循环;不要与循环中的 break 混淆。
|
|
|
+
|
|
|
+## 2. switch 按条件选择
|
|
|
+
|
|
|
+```go
|
|
|
+switch {
|
|
|
+case score >= 90:
|
|
|
+ fmt.Println("优秀")
|
|
|
+case score >= 60:
|
|
|
+ fmt.Println("及格")
|
|
|
+default:
|
|
|
+ fmt.Println("不及格")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+不写表达式时,可以理解为 switch true,每个 case 是布尔条件。按顺序找到第一个成立的条件,执行后就结束选择。
|
|
|
+
|
|
|
+和 else if 链一样,要注意先后顺序:先判断较高分数,避免 `>= 60` 提前匹配所有优秀分数。完整例程还先排除了非法成绩。
|
|
|
+
|
|
|
+## 3. defer:延迟调用,不是延迟到程序结束
|
|
|
+
|
|
|
+```go
|
|
|
+func showOrder() {
|
|
|
+ fmt.Println("进入函数")
|
|
|
+ defer fmt.Println("返回前执行")
|
|
|
+ fmt.Println("执行主体")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+输出顺序是:进入函数 → 执行主体 → 返回前执行。
|
|
|
+
|
|
|
+执行到 defer 语句时,会登记一个稍后执行的函数调用。当前函数正常返回前,再执行已经登记的调用,然后把控制权交回调用方。
|
|
|
+
|
|
|
+即使提前 return,也会执行此前已登记的 defer。没有走到的 defer 不会凭空执行。
|
|
|
+
|
|
|
+defer 要跟一个函数调用:`defer fmt.Println("结束")`。不是写 `defer fmt.Println`,也不是直接写 `defer { ... }`。
|
|
|
+
|
|
|
+## 4. 多个 defer:后登记,先执行
|
|
|
+
|
|
|
+```go
|
|
|
+defer fmt.Println("A")
|
|
|
+defer fmt.Println("B")
|
|
|
+fmt.Println("主体")
|
|
|
+```
|
|
|
+
|
|
|
+输出:
|
|
|
+
|
|
|
+```text
|
|
|
+主体
|
|
|
+B
|
|
|
+A
|
|
|
+```
|
|
|
+
|
|
|
+这叫后进先出(LIFO),可以先记成“最后登记的,最先执行”。defer 不会因为离开 if 或 for 的大括号就执行,它归当前函数管理。
|
|
|
+
|
|
|
+因此,不要在可能长时间运行的大循环中无限登记清理操作:它们会等函数退出才执行。后续文件课会学习如何及时释放资源。
|
|
|
+
|
|
|
+## 5. 参数立即求值,调用延后执行
|
|
|
+
|
|
|
+```go
|
|
|
+value := 10
|
|
|
+defer fmt.Println(value)
|
|
|
+value = 20
|
|
|
+```
|
|
|
+
|
|
|
+这个 defer 最终打印 10。因为登记时已经计算并保存了传给 Println 的参数值。后面修改变量,不会改变此前保存的这个整数参数。
|
|
|
+
|
|
|
+这里指普通调用参数的求值时机。后续匿名函数和闭包会涉及不同的变量访问方式,先不要混在一起理解。
|
|
|
+
|
|
|
+## 6. 典型用途和边界
|
|
|
+
|
|
|
+常见用途包括关闭文件、释放锁等。但应在资源成功获取后再登记相应清理,不是无条件 defer 一个可能无效的对象。
|
|
|
+
|
|
|
+- 普通错误通过 return 返回,defer 不会自动吞掉错误或把失败变成功。
|
|
|
+- defer 也参与 panic 展开时的清理,但不是自动恢复机制,后续再学习。
|
|
|
+- `os.Exit` 等直接终止进程的方式不会执行已登记的 defer,不要把它理解成所有情况下都保证执行。
|
|
|
+- 本课用 Println 演示时机,文件关闭的错误如何处理留到文件课。
|
|
|
+
|
|
|
+## 编译并运行
|
|
|
+
|
|
|
+在**仓库根目录**执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+New-Item -ItemType Directory -Force bin/12_switch_defer | Out-Null
|
|
|
+go build -o ./bin/12_switch_defer/switch_defer.exe ./lessons/12_switch_defer
|
|
|
+.\bin\12_switch_defer\switch_defer.exe
|
|
|
+```
|
|
|
+
|
|
|
+修改源码后需要重新编译。快速编译并运行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go run ./lessons/12_switch_defer
|
|
|
+```
|
|
|
+
|
|
|
+或从根目录进入本课:
|
|
|
+
|
|
|
+```powershell
|
|
|
+Set-Location lessons/12_switch_defer
|
|
|
+go run .
|
|
|
+```
|
|
|
+
|
|
|
+## 预期输出
|
|
|
+
|
|
|
+```text
|
|
|
+=== 按值选择 ===
|
|
|
+选择:编译并运行程序
|
|
|
+
|
|
|
+=== 多个值共用分支 ===
|
|
|
+周末
|
|
|
+
|
|
|
+=== 按条件选择 ===
|
|
|
+等级:良好
|
|
|
+
|
|
|
+=== defer 执行顺序 ===
|
|
|
+进入 showOrder
|
|
|
+执行函数主体
|
|
|
+延迟操作 B:先于 A 执行
|
|
|
+延迟操作 A:最后执行
|
|
|
+showOrder 已返回 main
|
|
|
+
|
|
|
+=== 提前返回也执行 defer ===
|
|
|
+checkScore:本次检查结束
|
|
|
+调用方收到错误: 成绩 120 无效
|
|
|
+
|
|
|
+=== defer 参数求值时机 ===
|
|
|
+主体中修改后的变量:20
|
|
|
+defer 使用登记时的参数:10
|
|
|
+```
|
|
|
+
|
|
|
+## 动手练习
|
|
|
+
|
|
|
+- [ ] 把 command 改为 build、test、未知文本,观察各分支。
|
|
|
+- [ ] 把 day 改为 1、7、8,检查匹配情况。
|
|
|
+- [ ] 用 59、60、79、80、89、90、100、101 检查成绩分支。
|
|
|
+- [ ] 交换两个 defer 的登记顺序,先预测再运行。
|
|
|
+- [ ] 把 checkScore 输入改为 85,观察主体输出、defer 输出和调用方输出的顺序。
|
|
|
+- [ ] 把 defer 参数的初值改成 30,随后改成 40,确认最终延迟输出仍为 30。
|
|
|
+
|
|
|
+若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/12_switch_defer/practice` 运行,不要在同一个包中重复声明 main 或同名函数。
|
|
|
+
|
|
|
+下一课学习函数进阶:匿名函数、函数作为参数和简单闭包,为理解测试里的 t.Run 做准备。
|
|
|
+
|
|
|
+[上一课:错误处理](../11_errors/README.md) · [学习总览](../../README.md)
|