深色模式
Go语言方法
概述
Go 没有类,但并不缺“把行为绑定到类型上”的能力。方法就是这条绑定线。它让一个类型不仅有数据结构,也有围绕这个结构组织起来的行为。
方法这件事真正容易混的地方不在语法,而在三个判断点:
- 接收者到底该用值还是指针
- 方法到底绑定在类型值上,还是绑定在类型指针上
- 方法集为什么会直接影响接口实现
把这三件事看清楚,Go 的方法模型就不会显得绕。
方法是什么
方法本质上是“带接收者的函数”:
go
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}这里的 Rectangle 就是接收者类型。
和普通函数相比,方法的差别不在能力,而在组织方式。普通函数更像“对一组输入做计算”,方法更像“这是某个类型天然应该有的行为”。
方法和函数的区别,重点在绑定关系
例如:
go
func Area(r Rectangle) float64 {
return r.Width * r.Height
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}两者都能工作,但后者更像是在表达:“面积这个行为属于 Rectangle 自己。”
方法可以定义在哪些类型上
方法不只可以定义在结构体上,也可以定义在自定义类型上:
go
type Celsius float64
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", c)
}这也是 Go 类型系统里“定义新类型”和“只是起别名”差别很大的原因之一。只有真正的新定义类型,才是方法绑定的正常载体。
值接收者和指针接收者
最常见的接收者有两种:
- 值接收者
- 指针接收者
值接收者
go
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}值接收者拿到的是一份接收者副本。它更适合:
- 方法不需要修改接收者状态
- 类型本身更像值对象
- 拷贝成本比较低
指针接收者
go
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}指针接收者更适合:
- 方法需要修改接收者
- 结构体较大,不想频繁拷贝
- 类型整体更偏指针语义
- 需要和其他指针接收者方法保持一致
接收者选择的关键,不只是性能
很多资料讲值接收者和指针接收者时,只盯着“拷贝大对象更慢”。这点当然重要,但不是最先该看的。
更稳的顺序通常是:
- 这个类型表达的是值,还是对象状态。
- 方法是否要修改接收者。
- 整个类型的方法集是否需要统一。
- 最后才看拷贝成本。
方法调用时的一个常见便利
Go 在方法调用上做了一些便利处理。只要值可寻址,调用值或指针方法时常常看起来都能直接写:
go
r.Scale(2)哪怕 Scale 定义在 *Rectangle 上,只要 r 是可取地址的变量,编译器通常会帮忙取址。
这很方便,但不要因为“能调通”就忽略方法集差异。调用便利不等于类型系统边界消失了。
方法集才是底层规则
方法集可以简单理解成“一个类型真正拥有的方法集合”。
例如:
go
type MyType struct{}
func (m MyType) ValueMethod() {}
func (m *MyType) PointerMethod() {}这里:
MyType的方法集里有ValueMethod*MyType的方法集里有ValueMethod和PointerMethod
这条规则会直接影响接口实现。
方法集为什么会影响接口
例如:
go
type Scaler interface {
Scale(float64)
}如果 Scale 是 *Rectangle 的方法,那么:
Rectangle值不实现Scaler*Rectangle实现Scaler
也就是说,“调用时看起来能写 r.Scale()”这件事,并不能推出“值类型也实现了这个接口”。
嵌入会带来方法提升
如果结构体嵌入了另一个类型,相关方法可能会被提升:
go
type Animal struct{}
func (a Animal) Eat() {}
type Dog struct {
Animal
}这时 Dog 可以直接调用 Eat()。但这仍然是组合和提升,不是类继承那套模型。
一条够用的判断线
看 Go 方法时,按这个顺序想通常比较稳:
- 这个行为是否天然属于某个类型。
- 它要不要修改接收者。
- 接收者整体该走值语义还是指针语义。
- 这个方法最终会不会进入某个接口的方法集判断。
