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