Просмотр исходного кода

feat: 新增第十课方法与接收者例程及学习说明

zhuts 3 дней назад
Родитель
Сommit
6e815f0026
3 измененных файлов с 283 добавлено и 3 удалено
  1. 7 3
      README.md
  2. 197 0
      lessons/10_methods/README.md
  3. 79 0
      lessons/10_methods/main.go

+ 7 - 3
README.md

@@ -37,9 +37,12 @@
     ├── 08_structs/         # 第 08 课:结构体
     │   ├── README.md       # 定义、字段、复制和结构体切片
     │   └── main.go         # 学生记录与简化测试数据表
-    └── 09_pointers/        # 第 09 课:指针
-        ├── README.md       # 取地址、解引用、nil 和参数传递
-        └── main.go         # 指针修改整数与结构体
+    ├── 09_pointers/        # 第 09 课:指针
+    │   ├── README.md       # 取地址、解引用、nil 和参数传递
+    │   └── main.go         # 指针修改整数与结构体
+    └── 10_methods/         # 第 10 课:方法与接收者
+        ├── README.md       # 值接收者、指针接收者与方法调用
+        └── main.go         # 学生信息、生日和成绩修改
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -57,6 +60,7 @@
 | 07 | [函数](./lessons/07_functions/README.md) | 参数、返回值、值传递、多返回值、可变参数 | 已建立 |
 | 08 | [结构体](./lessons/08_structs/README.md) | 定义、字段访问、零值、复制、结构体切片 | 已建立 |
 | 09 | [指针](./lessons/09_pointers/README.md) | &、*、nil、指针参数、结构体指针、new | 已建立 |
+| 10 | [方法与接收者](./lessons/10_methods/README.md) | 方法声明、值接收者、指针接收者、nil 处理 | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 197 - 0
lessons/10_methods/README.md

@@ -0,0 +1,197 @@
+# 第 10 课:方法与接收者
+
+## 学习目标
+
+学过结构体和指针后,本课把函数关联到类型上,学习方法、值接收者、指针接收者,读懂 `student.Birthday()` 和测试中的 `t.Fatalf(...)`。
+
+先手敲 [main.go](./main.go),按注释逐段理解。本课不引入接口、继承、匿名函数或复杂测试。共享根目录的 `go.mod`,无需第三方依赖。
+
+## 1. 普通函数变成方法
+
+普通函数把学生放在参数列表里:
+
+```go
+func describe(student Student) string {
+    return student.Name
+}
+
+// 在 main 内调用:
+text := describe(student)
+```
+
+方法在 func 与方法名之间增加一个接收者:
+
+```go
+func (student Student) Summary() string {
+    return student.Name
+}
+
+// 在 main 内调用:
+text := student.Summary()
+```
+
+逐项理解:
+
+- `(student Student)`:接收者声明,student 是本方法内的变量名,Student 是类型。
+- `Summary`:方法名。
+- `()`:普通参数列表,本方法没有额外参数。
+- `string`:返回值类型。
+
+接收者名字不是关键字,也可以起名为 s;没有必须写 this 或 self 的要求。方法定义在包级,不写在 struct 的大括号里面,也不写在 main 内部。
+
+方法让调用方式变成“这个学生执行某种操作”。Go 不需要先定义 class,也没有 C++ 风格的类继承。当前只需理解类型与方法的关联。
+
+## 2. 值接收者:得到副本
+
+```go
+func (student Student) TryBirthday() {
+    student.Age++
+}
+```
+
+`student.TryBirthday()` 修改的是方法内的结构体副本,原学生的 Age 不变。本课 Student 字段是 string、int、float64;如果字段包含切片或 map,值接收者仍可能修改共享底层数据,不能简单理解为“值接收者绝对无法影响外部”。
+
+只读取这种简单结构体时,值接收者可以表达需求;如果要修改原值,使用指针接收者。
+
+## 3. 指针接收者:修改原值
+
+```go
+func (student *Student) Birthday() bool {
+    if student == nil {
+        return false
+    }
+    student.Age++
+    return true
+}
+```
+
+`*Student` 是指针类型,不是此处执行解引用。方法内的 `student.Age` 等价于 `(*student).Age`。
+
+接收者同样按值传递,只是复制了指针值,因此可以找到并修改原数据。函数和方法都没有改变 Go 的值传递规则。
+
+```go
+student := Student{Name: "小朱", Age: 20}
+student.Birthday()
+```
+
+student 是可取地址的变量,Go 会自动把这个调用理解为 `(&student).Birthday()`,不需要每次手动写 &。
+
+但不能据此认为任何 Student 值都能自动取地址:例如 map 中的结构体值不可取地址,不能直接调用它的指针接收者方法;需先取到局部变量,修改后写回,或按需求设计成保存指针。
+
+教学示例特意混用两种接收者作对比,不代表项目里必须混用。实际设计通常要考虑修改需求、复制成本和一致性;接口中的方法集规则留到接口课。
+
+## 4. 方法也有普通参数和返回值
+
+```go
+func (student *Student) SetScore(score float64) bool {
+    if student == nil || !(score >= 0 && score <= 100) {
+        return false
+    }
+    student.Score = score
+    return true
+}
+```
+
+- student:接收者,指出修改哪个学生。
+- score:普通参数,指出新成绩。
+- bool:返回值,说明是否成功。
+
+调用 `student.SetScore(95)` 会更新成绩;传入 120 返回 false,保留原成绩。
+
+`||` 会短路:student 是 nil 时,不必再判断后面的条件。范围条件写成“必须在 0~100 内”,也会拒绝特殊浮点值 NaN;现在先理解普通分数即可。
+
+## 5. 指针调用与 nil
+
+```go
+pointer := &student
+text := pointer.Summary()
+```
+
+非 nil 的 *Student 也能调用值接收者方法,调用时会取得相应 Student 值。
+
+nil 指针能否调用某个指针接收者方法,取决于实现是否能处理 nil。本课 Birthday 和 SetScore 主动检查,因此安全返回 false。
+
+不能因此认为所有方法都支持 nil:对 nil 指针调用本课的值接收者 Summary,需要先取得 Student 值,会发生 panic。直接访问 nil 指针字段也不安全。
+
+## 6. 再看 t.Fatalf
+
+```go
+func TestAdd(t *testing.T) {
+    got := add(2, 3)
+    if got != 5 {
+        t.Fatalf("实际结果:%d,期望:5", got)
+    }
+}
+```
+
+现在可以读懂:
+
+1. t 是指向 testing.T 的指针参数。
+2. Fatalf 是 testing.T 提供的指针接收者方法。
+3. `t.Fatalf(...)` 调用方法,传入格式字符串和数据。
+4. 该方法报告失败并停止当前测试的执行。
+
+这里的点号不是读取普通数据字段,而是选择方法并调用。测试框架提供 t,不需要自己 new 一个 testing.T。本课示例不包含 testing 包,上述片段只用于解释上一课测试。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/10_methods | Out-Null
+go build -o ./bin/10_methods/methods.exe ./lessons/10_methods
+.\bin\10_methods\methods.exe
+```
+
+修改源码后需要重新编译。快速编译并运行:
+
+```powershell
+go run ./lessons/10_methods
+```
+
+或从根目录进入本课:
+
+```powershell
+Set-Location lessons/10_methods
+go run .
+```
+
+## 预期输出
+
+```text
+=== 普通函数与方法 ===
+普通函数: 小朱,年龄:20,成绩:85.0
+方法调用: 小朱,年龄:20,成绩:85.0
+
+=== 值接收者 ===
+调用 TryBirthday 后,原年龄:20
+
+=== 指针接收者 ===
+调用 Birthday 后,年龄:21,成功:true
+
+=== 方法参数与返回值 ===
+设置 95 分:成功=true,当前成绩=95.0
+设置 120 分:成功=false,当前成绩=95.0
+
+=== 通过指针调用方法 ===
+小朱,年龄:21,成绩:95.0
+
+=== nil 指针接收者 ===
+nil 调用 Birthday:false
+nil 调用 SetScore:false
+```
+
+## 动手练习
+
+- [ ] 对比普通函数 describe 和 Summary 方法的调用方式。
+- [ ] 连续调用两次 Birthday,核对原学生年龄。
+- [ ] 将 TryBirthday 改为指针接收者,观察结果变化,再恢复。
+- [ ] 新增值接收者方法 `Passed() bool`,根据 Score 是否大于等于 60 返回结果。
+- [ ] 用 0、60、100、-1、101 检查 SetScore 的边界和失败后是否保留原成绩。
+- [ ] 回看第七课的 t.Fatalf,解释指针参数、方法名和普通参数。
+
+若另外手敲一份,放进本课 `practice/` 子目录,用 `go run ./lessons/10_methods/practice` 运行,不要在同一个包中重复声明 Student 或 main。
+
+下一课学习 error 错误处理,让失败结果不只返回 false,还能说明具体原因。
+
+[上一课:指针](../09_pointers/README.md) · [学习总览](../../README.md)

+ 79 - 0
lessons/10_methods/main.go

@@ -0,0 +1,79 @@
+package main
+
+import "fmt"
+
+type Student struct {
+	Name  string
+	Age   int
+	Score float64
+}
+
+// 1. 普通函数:学生作为普通参数传入。
+func describe(student Student) string {
+	return fmt.Sprintf("%s,年龄:%d,成绩:%.1f", student.Name, student.Age, student.Score)
+}
+
+// 2. 方法:func 后面的 (student Student) 称为接收者。
+// 值接收者接收 Student 值的副本,这个方法只读取字段。
+func (student Student) Summary() string {
+	return fmt.Sprintf("%s,年龄:%d,成绩:%.1f", student.Name, student.Age, student.Score)
+}
+
+// 3. 值接收者中修改整数,只修改副本,不改变原学生。
+func (student Student) TryBirthday() {
+	student.Age++
+}
+
+// 4. 指针接收者可以通过指针修改原学生。
+// 本方法约定允许 nil,返回 false 表示没有可修改的学生。
+func (student *Student) Birthday() bool {
+	if student == nil {
+		return false
+	}
+	student.Age++
+	return true
+}
+
+// 5. 方法也能接收普通参数,并通过返回值报告是否成功。
+func (student *Student) SetScore(score float64) bool {
+	// 这种写法同时拒绝不在范围内的值和 NaN。
+	if student == nil || !(score >= 0 && score <= 100) {
+		return false
+	}
+	student.Score = score
+	return true
+}
+
+func main() {
+	student := Student{Name: "小朱", Age: 20, Score: 85}
+
+	fmt.Println("=== 普通函数与方法 ===")
+	fmt.Println("普通函数:", describe(student))
+	fmt.Println("方法调用:", student.Summary())
+
+	fmt.Println("\n=== 值接收者 ===")
+	student.TryBirthday()
+	fmt.Printf("调用 TryBirthday 后,原年龄:%d\n", student.Age)
+
+	fmt.Println("\n=== 指针接收者 ===")
+	// student 是可以取地址的变量,Go 自动把调用理解为 (&student).Birthday()。
+	ok := student.Birthday()
+	fmt.Printf("调用 Birthday 后,年龄:%d,成功:%t\n", student.Age, ok)
+
+	fmt.Println("\n=== 方法参数与返回值 ===")
+	ok = student.SetScore(95)
+	fmt.Printf("设置 95 分:成功=%t,当前成绩=%.1f\n", ok, student.Score)
+	ok = student.SetScore(120)
+	fmt.Printf("设置 120 分:成功=%t,当前成绩=%.1f\n", ok, student.Score)
+
+	fmt.Println("\n=== 通过指针调用方法 ===")
+	pointer := &student
+	// *Student 也可以调用值接收者的方法,前提是这里指针非 nil。
+	fmt.Println(pointer.Summary())
+
+	fmt.Println("\n=== nil 指针接收者 ===")
+	var empty *Student
+	fmt.Printf("nil 调用 Birthday:%t\n", empty.Birthday())
+	fmt.Printf("nil 调用 SetScore:%t\n", empty.SetScore(80))
+	// empty.Summary() 不能安全调用:值接收者需要取得 Student 值。
+}