# 第 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)