1
0

2 Revīzijas 6e815f0026 ... 406e48076b

Autors SHA1 Ziņojums Datums
  zhuts 406e48076b feat: 新增第十二课switch与defer例程及学习说明 3 dienas atpakaļ
  zhuts e38f050a05 feat: 新增第十一课错误处理例程与学习说明 3 dienas atpakaļ

+ 11 - 3
README.md

@@ -40,9 +40,15 @@
     ├── 09_pointers/        # 第 09 课:指针
     │   ├── README.md       # 取地址、解引用、nil 和参数传递
     │   └── main.go         # 指针修改整数与结构体
-    └── 10_methods/         # 第 10 课:方法与接收者
-        ├── README.md       # 值接收者、指针接收者与方法调用
-        └── main.go         # 学生信息、生日和成绩修改
+    ├── 10_methods/         # 第 10 课:方法与接收者
+    │   ├── README.md       # 值接收者、指针接收者与方法调用
+    │   └── main.go         # 学生信息、生日和成绩修改
+    ├── 11_errors/          # 第 11 课:错误处理
+    │   ├── README.md       # error、nil、检查与错误传递
+    │   └── main.go         # 除法、成绩校验和字符串转换
+    └── 12_switch_defer/    # 第 12 课:switch 与 defer
+        ├── README.md       # 多分支、延迟调用与执行顺序
+        └── main.go         # 指令选择、成绩判断和 defer 示例
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -61,6 +67,8 @@
 | 08 | [结构体](./lessons/08_structs/README.md) | 定义、字段访问、零值、复制、结构体切片 | 已建立 |
 | 09 | [指针](./lessons/09_pointers/README.md) | &、*、nil、指针参数、结构体指针、new | 已建立 |
 | 10 | [方法与接收者](./lessons/10_methods/README.md) | 方法声明、值接收者、指针接收者、nil 处理 | 已建立 |
+| 11 | [错误处理](./lessons/11_errors/README.md) | error、nil、errors.New、fmt.Errorf、错误传递 | 已建立 |
+| 12 | [switch 与 defer](./lessons/12_switch_defer/README.md) | 多分支、延迟调用、后进先出、参数求值 | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 161 - 0
lessons/11_errors/README.md

@@ -0,0 +1,161 @@
+# 第 11 课:error 错误处理
+
+## 学习目标
+
+之前用 bool 表达成功或失败,现在让函数返回失败原因。学会 `(结果, error)`、`if err != nil`、提前返回,以及向调用方传递错误。
+
+按 [main.go](./main.go) 逐段手敲。本课只使用标准库 errors、fmt、strconv,无需安装依赖。暂不引入自定义错误类型、错误包装、panic / recover 或匿名函数。
+
+## 1. 成功返回 nil,失败返回 error
+
+```go
+func divide(a, b int) (float64, error) {
+    if b == 0 {
+        return 0, errors.New("除数不能为 0")
+    }
+    return float64(a) / float64(b), nil
+}
+```
+
+逐项理解:
+
+- `(float64, error)`:两个返回值,第一个是结果,第二个是错误。
+- `error`:Go 预定义的接口类型,现在先理解成“保存错误信息的类型”,接口细节后续学习。
+- `errors.New(...)`:根据一段文字创建错误值,不会自动打印,也不会自动终止程序。
+- `nil`:在这里表示没有错误,不是字符串 `"nil"`。
+- 失败时返回的 0 是占位结果,本函数约定出现错误时不要使用它。
+
+error 不是关键字,也不是异常抛出语法。Go 的这种常规错误处理依靠返回值和显式判断。
+
+## 2. 调用后先检查错误
+
+```go
+result, err := divide(10, 4)
+if err != nil {
+    fmt.Println("计算失败:", err)
+} else {
+    fmt.Println("计算结果:", result)
+}
+```
+
+err 是普通变量名,习惯用它保存错误。`err != nil` 表示发生了错误;`err == nil` 表示本次调用成功。
+
+不要通过结果是不是 0 判断成功:0 本身可能是合法结果。例如本课 `parseScore("0")` 返回 `(0, nil)`。
+
+本课为方便观察,在 main 中既展示成功也展示失败。实际辅助函数中常写成:
+
+```go
+result, err := divide(10, 4)
+if err != nil {
+    return err // 这段需要放在返回 error 的函数中。
+}
+// 下面只处理成功路径,使用 result。
+```
+
+这个 return 只退出当前函数,不自动退出整个程序。返回类型必须匹配,不能在无返回值的 main 中直接写 `return err`。
+
+本课失败时约定忽略结果;标准库中某些函数会同时返回部分结果和错误,应以具体文档约定为准。
+
+## 3. 只有 error 的函数
+
+```go
+func validateScore(score int) error {
+    if score < 0 || score > 100 {
+        return fmt.Errorf("成绩 %d 无效,应在 0~100 之间", score)
+    }
+    return nil
+}
+```
+
+有些操作没有额外结果,只有“成功或失败”,就只返回 error。
+
+`fmt.Errorf` 创建带格式的错误信息,支持 `%d`、`%s` 等占位符;它与 `fmt.Printf` 不同,**不会直接输出到终端**。
+
+## 4. 调用标准库并传递错误
+
+```go
+score, err := strconv.Atoi(text)
+if err != nil {
+    return 0, err
+}
+```
+
+`strconv.Atoi` 将十进制字符串转换为 int;例如 `"85"` 成功,`"abc"` 失败。超出 int 可表示范围也会失败。它不会自动去掉字符串两端的空白。
+
+parseScore 的执行顺序:
+
+1. 把文本转成整数,转换失败立即返回错误。
+2. 校验整数是否在 0~100 之间,校验失败立即返回错误。
+3. 两步都通过,返回整数和 nil。
+
+函数内部负责计算和返回错误,main 负责决定怎样显示、继续还是停止,避免同一个错误在每一层重复打印。
+
+源码中第二次给 err 赋值使用 `err = validateScore(score)`,因为 err 已存在,不需要再次用 `:=`。
+
+## 5. 错误不等于 panic
+
+普通 error 是可供调用方检查的返回值,适合无效输入、找不到文件、网络失败等情况。只要调用方处理得当,程序可以继续执行。
+
+panic 是另一种运行时机制,例如解引用 nil 指针可能引发 panic;它不是 error 返回值,也不会自动被 `if err != nil` 捕获。本课不使用 panic 处理普通输入错误。
+
+不要为了省事写 `score, _ := strconv.Atoi(text)` 忽略错误,否则转换失败可能被误当作合法的 0。以后学习错误分类时,也不要仅凭错误文字来判断错误种类。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/11_errors | Out-Null
+go build -o ./bin/11_errors/errors.exe ./lessons/11_errors
+.\bin\11_errors\errors.exe
+```
+
+修改源码后需要重新编译。快速编译并运行:
+
+```powershell
+go run ./lessons/11_errors
+```
+
+或从根目录进入本课:
+
+```powershell
+Set-Location lessons/11_errors
+go run .
+```
+
+## 预期输出
+
+```text
+=== 成功返回 nil ===
+10 / 4 = 2.50
+
+=== 失败返回原因 ===
+计算失败: 除数不能为 0
+
+=== 只有 error 返回值 ===
+校验失败: 成绩 120 无效,应在 0~100 之间
+
+=== 字符串转换与错误传递 ===
+输入 "85" 成功:成绩=85
+输入 "0" 成功:成绩=0
+输入 "120" 失败:成绩 120 无效,应在 0~100 之间
+输入 "abc" 失败:strconv.Atoi: parsing "abc": invalid syntax
+处理结束:错误已由调用方处理,程序继续运行
+```
+
+标准库错误文字可能随版本变化,理解含义即可,不要把完整字符串当作稳定接口。
+
+## 动手练习
+
+- [ ] 把除数改成 2、-2、0,观察成功和失败分支。
+- [ ] 添加输入 `"100"`、`"-1"`、`"101"`、`"80.5"`,先预测再运行。
+- [ ] 试一下 `" 85 "`,确认空白不会被 Atoi 自动忽略。
+- [ ] 修改校验错误信息,观察 fmt.Errorf 返回的错误在 main 中如何显示。
+- [ ] 新写 `checkAge(age int) error`,为自己约定的合法年龄范围返回 nil 或错误。
+- [ ] 回看上一课 SetScore 的 bool 返回值,思考改成 error 后怎样报告具体原因。
+
+若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/11_errors/practice` 运行,不要在同一个包中重复定义 main 或同名函数。
+
+下一课学习 switch 与 defer,分别处理多分支选择和函数退出前的操作。
+
+[上一课:方法与接收者](../10_methods/README.md) · [学习总览](../../README.md)

+ 75 - 0
lessons/11_errors/main.go

@@ -0,0 +1,75 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+)
+
+// 1. 常见返回形式:(计算结果, 错误)。
+// 成功时 error 返回 nil,失败时返回描述原因的错误值。
+func divide(a, b int) (float64, error) {
+	if b == 0 {
+		return 0, errors.New("除数不能为 0")
+	}
+	return float64(a) / float64(b), nil
+}
+
+// 2. fmt.Errorf 可以像 Printf 一样,将具体数据放进错误信息。
+func validateScore(score int) error {
+	if score < 0 || score > 100 {
+		return fmt.Errorf("成绩 %d 无效,应在 0~100 之间", score)
+	}
+	return nil
+}
+
+// 3. 调用可能失败的函数后,先检查错误,再使用结果。
+// 下层函数返回错误时,可以直接把错误交给上层调用方。
+func parseScore(text string) (int, error) {
+	score, err := strconv.Atoi(text) // Atoi 将字符串转换为 int。
+	if err != nil {
+		return 0, err
+	}
+
+	err = validateScore(score)
+	if err != nil {
+		return 0, err
+	}
+	return score, nil
+}
+
+func main() {
+	fmt.Println("=== 成功返回 nil ===")
+	result, err := divide(10, 4)
+	if err != nil {
+		fmt.Println("计算失败:", err)
+	} else {
+		fmt.Printf("10 / 4 = %.2f\n", result)
+	}
+
+	fmt.Println("\n=== 失败返回原因 ===")
+	result, err = divide(10, 0)
+	if err != nil {
+		fmt.Println("计算失败:", err)
+	} else {
+		fmt.Printf("计算结果:%.2f\n", result)
+	}
+
+	fmt.Println("\n=== 只有 error 返回值 ===")
+	err = validateScore(120)
+	if err != nil {
+		fmt.Println("校验失败:", err)
+	}
+
+	fmt.Println("\n=== 字符串转换与错误传递 ===")
+	inputs := []string{"85", "0", "120", "abc"}
+	for _, input := range inputs {
+		score, err := parseScore(input)
+		if err != nil {
+			fmt.Printf("输入 %q 失败:%v\n", input, err)
+			continue // 本次失败不使用 score,继续处理下一项。
+		}
+		fmt.Printf("输入 %q 成功:成绩=%d\n", input, score)
+	}
+	fmt.Println("处理结束:错误已由调用方处理,程序继续运行")
+}

+ 172 - 0
lessons/12_switch_defer/README.md

@@ -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)

+ 87 - 0
lessons/12_switch_defer/main.go

@@ -0,0 +1,87 @@
+package main
+
+import "fmt"
+
+// defer 在当前函数即将返回时执行,而不是整个程序结束时才执行。
+func showOrder() {
+	fmt.Println("进入 showOrder")
+	defer fmt.Println("延迟操作 A:最后执行")
+	defer fmt.Println("延迟操作 B:先于 A 执行")
+	fmt.Println("执行函数主体")
+}
+
+// 即使提前 return,之前已经登记的 defer 也会执行。
+func checkScore(score int) error {
+	defer fmt.Println("checkScore:本次检查结束")
+	if score < 0 || score > 100 {
+		return fmt.Errorf("成绩 %d 无效", score)
+	}
+	fmt.Printf("成绩 %d 有效\n", score)
+	return nil
+}
+
+// defer 调用的参数在登记时求值,不是等到返回时才读取变量。
+func showArgument() {
+	value := 10
+	defer fmt.Printf("defer 使用登记时的参数:%d\n", value)
+	value = 20
+	fmt.Printf("主体中修改后的变量:%d\n", value)
+}
+
+func main() {
+	// 1. 根据一个值选择分支,不像 C 那样默认贯穿到后面的 case。
+	fmt.Println("=== 按值选择 ===")
+	command := "run"
+	switch command {
+	case "build":
+		fmt.Println("选择:编译程序")
+	case "run":
+		fmt.Println("选择:编译并运行程序")
+	case "test":
+		fmt.Println("选择:运行测试")
+	default:
+		fmt.Println("未知指令")
+	}
+
+	// 2. 同一个 case 可以列出多个值。
+	fmt.Println("\n=== 多个值共用分支 ===")
+	day := 6
+	switch day {
+	case 1, 2, 3, 4, 5:
+		fmt.Println("工作日")
+	case 6, 7:
+		fmt.Println("周末")
+	default:
+		fmt.Println("星期数无效")
+	}
+
+	// 3. 不写 switch 表达式,每个 case 写布尔条件。
+	// 从上到下判断,只执行第一个成立的分支。
+	fmt.Println("\n=== 按条件选择 ===")
+	score := 85
+	switch {
+	case score < 0 || score > 100:
+		fmt.Println("成绩无效")
+	case score >= 90:
+		fmt.Println("等级:优秀")
+	case score >= 80:
+		fmt.Println("等级:良好")
+	case score >= 60:
+		fmt.Println("等级:及格")
+	default:
+		fmt.Println("等级:不及格")
+	}
+
+	fmt.Println("\n=== defer 执行顺序 ===")
+	showOrder()
+	fmt.Println("showOrder 已返回 main")
+
+	fmt.Println("\n=== 提前返回也执行 defer ===")
+	err := checkScore(120)
+	if err != nil {
+		fmt.Println("调用方收到错误:", err)
+	}
+
+	fmt.Println("\n=== defer 参数求值时机 ===")
+	showArgument()
+}