zhuts 6e815f0026 feat: 新增第十课方法与接收者例程及学习说明 3 дней назад
..
README.md 6e815f0026 feat: 新增第十课方法与接收者例程及学习说明 3 дней назад
main.go 6e815f0026 feat: 新增第十课方法与接收者例程及学习说明 3 дней назад

README.md

第 10 课:方法与接收者

学习目标

学过结构体和指针后,本课把函数关联到类型上,学习方法、值接收者、指针接收者,读懂 student.Birthday() 和测试中的 t.Fatalf(...)。

先手敲 main.go,按注释逐段理解。本课不引入接口、继承、匿名函数或复杂测试。共享根目录的 go.mod,无需第三方依赖。

1. 普通函数变成方法

普通函数把学生放在参数列表里:

func describe(student Student) string {
    return student.Name
}

// 在 main 内调用:
text := describe(student)

方法在 func 与方法名之间增加一个接收者:

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. 值接收者:得到副本

func (student Student) TryBirthday() {
    student.Age++
}

student.TryBirthday() 修改的是方法内的结构体副本,原学生的 Age 不变。本课 Student 字段是 string、int、float64;如果字段包含切片或 map,值接收者仍可能修改共享底层数据,不能简单理解为“值接收者绝对无法影响外部”。

只读取这种简单结构体时,值接收者可以表达需求;如果要修改原值,使用指针接收者。

3. 指针接收者:修改原值

func (student *Student) Birthday() bool {
    if student == nil {
        return false
    }
    student.Age++
    return true
}

*Student 是指针类型,不是此处执行解引用。方法内的 student.Age 等价于 (*student).Age。

接收者同样按值传递,只是复制了指针值,因此可以找到并修改原数据。函数和方法都没有改变 Go 的值传递规则。

student := Student{Name: "小朱", Age: 20}
student.Birthday()

student 是可取地址的变量,Go 会自动把这个调用理解为 (&student).Birthday(),不需要每次手动写 &。

但不能据此认为任何 Student 值都能自动取地址:例如 map 中的结构体值不可取地址,不能直接调用它的指针接收者方法;需先取到局部变量,修改后写回,或按需求设计成保存指针。

教学示例特意混用两种接收者作对比,不代表项目里必须混用。实际设计通常要考虑修改需求、复制成本和一致性;接口中的方法集规则留到接口课。

4. 方法也有普通参数和返回值

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

pointer := &student
text := pointer.Summary()

非 nil 的 *Student 也能调用值接收者方法,调用时会取得相应 Student 值。

nil 指针能否调用某个指针接收者方法,取决于实现是否能处理 nil。本课 Birthday 和 SetScore 主动检查,因此安全返回 false。

不能因此认为所有方法都支持 nil:对 nil 指针调用本课的值接收者 Summary,需要先取得 Student 值,会发生 panic。直接访问 nil 指针字段也不安全。

6. 再看 t.Fatalf

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 包,上述片段只用于解释上一课测试。

编译并运行

在仓库根目录执行:

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

修改源码后需要重新编译。快速编译并运行:

go run ./lessons/10_methods

或从根目录进入本课:

Set-Location lessons/10_methods
go run .

预期输出

=== 普通函数与方法 ===
普通函数: 小朱,年龄: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,还能说明具体原因。

上一课:指针 · 学习总览