Go 从 0 到精通 · 第 13 课:方法 method
Go 从 0 到精通 · 第 13 课:方法 method
学习定位:这是整套 Go 教程的第 13 课,也是阶段二(核心语法阶段)的最后一课。
前置要求:已经完成第 12 课,掌握了结构体的定义、初始化、字段访问、嵌套和值语义。
本课目标:理解 Go 中"数据 + 行为"的组织方式,掌握值接收者和指针接收者的区别,能为结构体定义相关行为。
1. 本课你要解决的核心问题
第 12 课你学会了用结构体把数据打包在一起。但数据本身没有行为——你只能在外面写函数来操作它:
1 | type Student struct { |
方法让你把操作"绑定"到类型上,实现"数据 + 行为"的统一:
1 | func (s *Student) Birthday() { |
你需要搞明白以下问题:
- 方法和普通函数有什么区别
- 值接收者和指针接收者分别怎么用
- 什么时候用值接收者,什么时候用指针接收者
- 方法的本质是什么
- 方法有哪些常见坑
学完这一课,你就完成了阶段二的全部内容,掌握了 Go 中组织程序的核心工具。
2. 方法的基本概念
2.1 什么是方法
方法就是绑定了接收者(receiver)的函数。接收者写在 func 关键字和方法名之间:
1 | func (接收者变量 接收者类型) 方法名(参数列表) 返回值列表 { |
对比普通函数:
1 | // 普通函数 |
2.2 调用方式
1 | func main() { |
p.Distance() 表示"在 p 上执行 Distance 操作"——这比 Distance(p) 更有表达力。
3. 值接收者
3.1 基本用法
值接收者的方法,接收的是结构体的副本:
1 | package main |
(c Counter) 表示接收者是 Counter 类型的值(副本)。
3.2 值接收者不能修改原结构体
1 | package main |
Increment 拿到的是 ct 的副本,c.Value++ 修改的是副本,ct 本身没有变。
这和第 12 课的值传递问题是一样的——值接收者就是值传递。
4. 指针接收者
4.1 基本用法
指针接收者的方法,接收的是结构体的指针,可以修改原结构体:
1 | package main |
(*Counter) 表示接收者是指针。Go 的语法糖让你不需要写 (*c).Value++,直接用 c.Value++。
4.2 指针接收者 vs 值接收者
| 特性 | 值接收者 | 指针接收者 |
|---|---|---|
| 语法 | (c Counter) |
(c *Counter) |
| 拿到的是 | 副本 | 指针(共享) |
| 能否修改原结构体 | 不能 | 能 |
| 调用时 | ct.Show() |
ct.Increment() |
5. 调用时的自动转换
Go 编译器在方法调用时会做一些自动转换,让你不需要手动取址或解引用。
5.1 值变量可以调用指针接收者的方法
1 | type Counter struct { |
Go 自动帮你取了地址。
5.2 指针变量可以调用值接收者的方法
1 | func (c Counter) Show() { |
Go 自动帮你解了引用。
5.3 总结
不管接收者是指针还是值,调用方式都一样:
变量.方法名()。Go 编译器帮你搞定转换。
6. 什么时候用指针接收者
这是实践中最重要的判断之一。
6.1 必须用指针接收者的情况
- 方法需要修改接收者:比如
Increment、SetName - 结构体很大:值接收者每次调用都复制整个结构体
- 保持一致性:如果一个类型的某个方法用了指针接收者,其他方法也建议用指针接收者
6.2 可以用值接收者的情况
- 方法不需要修改接收者:比如
Show、String、只读查询 - 结构体很小:复制成本低
- 并发安全考虑:值接收者天然不共享数据
6.3 实践建议
如果不确定,用指针接收者。大多数实际项目中的方法都使用指针接收者,因为结构体通常需要被修改,而且避免复制。
Go 标准库中的常见类型(bytes.Buffer、strings.Builder、http.Request)的方法基本都是指针接收者。
7. 方法的本质
方法本质上就是语法糖。编译器会把方法调用转换成普通函数调用:
1 | ct.Increment() |
接收者就是第一个参数。值接收者是值传递,指针接收者是指针传递。
这和你第 6 课学的"Go 的参数传递"完全一致。方法只是让代码看起来更面向对象。
8. 给非结构体类型定义方法
方法不仅可以绑定到结构体,还可以绑定到任何自定义类型:
1 | package main |
但你不能给其他包的类型定义方法——只能给当前包中定义的类型。
也不能给内置类型(如 int、string)直接定义方法,必须先用 type 创建新类型。
9. String() 方法:自定义打印格式
Go 中有一个特殊的约定:如果你的类型实现了 String() string 方法,fmt.Println 和 fmt.Printf 会自动调用它:
1 | package main |
fmt.Println 会检查类型是否实现了 String() 方法,如果有就调用它来获取字符串表示。
这是 Go 中最常用的"行为定制"模式之一。
10. 一段综合示例
1 | package main |
输出:
1 | Alice 的账户,余额:1000.00 |
11. 常见坑总结
11.1 值接收者的方法无法修改接收者
1 | func (c Counter) Increment() { c.Value++ } // 改的是副本 |
需要修改就用指针接收者。
11.2 混用值接收者和指针接收者
1 | type Counter struct { Value int } |
虽然 Go 允许这样做,但混用会让代码风格不一致。建议:一个类型的全部方法要么都用值接收者,要么都用指针接收者。如果需要修改就统一用指针接收者。
11.3 在 nil 接收者上调用方法
1 | func (a *BankAccount) Info() string { |
如果接收者可能是 nil,方法内部要做检查。但通常情况下,不需要主动处理 nil 接收者——确保不传 nil 就行。
11.4 以为方法需要定义在同一个文件里
Go 允许同一个类型的方法分散在多个文件中,只要它们在同一个包里。但为了可读性,通常把一个类型的所有方法放在一起。
11.5 不能给其他包的类型定义方法
1 | // 不能这样做: |
只能给当前包中定义的类型或自定义类型定义方法。
12. 阶段二完成回顾
到这里,你已经完成了阶段二:核心语法阶段的全部 7 课内容。
回顾一下你现在的能力:
| 课次 | 主题 | 你获得的能力 |
|---|---|---|
| 第 07 课 | 指针入门 | 理解指针、& 和 *,能在函数里修改外部变量 |
| 第 08 课 | 数组 | 理解固定长度集合,知道数组是值类型 |
| 第 09 课 | 切片 | 掌握动态数组,append、截取、容量管理 |
| 第 10 课 | 映射 map | 掌握键值对查找、增删改查、遍历 |
| 第 11 课 | 字符串与编码 | 理解 byte/rune、正确处理中文 |
| 第 12 课 | 结构体 | 能用结构体建模业务对象 |
| 第 13 课 | 方法 | 能为类型定义行为,理解值接收者与指针接收者 |
你现在已经具备了用 Go 组织中等复杂度程序的完整能力。接下来我们进入阶段三:核心抽象阶段,开始学习接口、组合、错误处理这些更高层次的设计能力。
13. 本课练习
练习 1:为结构体添加方法
要求:
- 定义
Circle结构体(Radius float64) - 添加
Area()方法(值接收者,返回面积) - 添加
Scale(factor float64)方法(指针接收者,缩放半径) - 测试调用
练习 2:值接收者 vs 指针接收者
要求:
- 定义
Score结构体(Value int) - 定义值接收者方法
Display() - 定义指针接收者方法
Add(n int) - 验证
Display不修改原值,Add修改原值
练习 3:自定义 String() 方法
要求:
- 定义
Color结构体(R,G,B uint8) - 实现
String() string方法,返回"rgb(R, G, B)" - 用
fmt.Println验证自动调用
练习 4:链式调用
要求:
- 定义
StringBuilder结构体(内部用[]byte存储) - 实现
Append(s string) *StringBuilder方法 - 实现
String() string方法 - 支持链式调用:
sb.Append("Hello").Append(" ").Append("Go").String()
提示:Append 返回 *StringBuilder(接收者本身)。
练习 5:综合项目——简单银行系统
要求:
- 定义
Account结构体(Owner、Balance) - 实现
Deposit、Withdraw、Transfer方法 Withdraw余额不足时返回错误Transfer从一个账户转到另一个- 所有修改操作用指针接收者
练习 6:给自定义类型定义方法
要求:
type Celsius float64、type Fahrenheit float64- 给
Celsius添加ToFahrenheit()方法 - 给
Fahrenheit添加ToCelsius()方法 - 给两个类型都实现
String()方法,分别显示"XX°C"和"XX°F" - 测试
fmt.Println(celsius值)和fmt.Println(fahrenheit值)
14. 自测题
14.1 概念题
- 方法和普通函数的区别是什么?
- 接收者写在函数签名的什么位置?
- 值接收者和指针接收者的核心区别是什么?
- 值接收者的方法能修改原结构体吗?
- 值变量能调用指针接收者的方法吗?Go 怎么处理的?
- 什么时候应该用指针接收者?
String()方法有什么特殊之处?- 方法本质上是什么?
- 能给其他包的类型定义方法吗?
- 一个类型的方法可以混用值接收者和指针接收者吗?推荐吗?
15. 本课总结
这一课你学到了 Go 中"数据 + 行为"的组织方式——方法。
你现在应该已经理解:
- 方法是绑定了接收者的函数
- 值接收者拿到副本,不能修改原结构体
- 指针接收者拿到指针,可以修改原结构体
- 调用时 Go 自动做取址/解引用转换
- 需要修改接收者或结构体较大时用指针接收者
- 实现
String() string可以自定义打印格式 - 不能给其他包的类型定义方法
16. 下一课预告
下一课我们进入阶段三:核心抽象阶段,首先学习:接口 interface 入门。
会重点讲:
- 接口是什么,Go 的接口和其他语言有什么不同
- 什么是"鸭子类型"
- 怎么定义接口、怎么实现接口
- 空接口
interface{}有什么用 - 接口在实际开发中的价值
学完下一课,你就能理解 Go 的抽象能力来自接口而非继承,开始用更高级的方式组织代码。





