Go 方法 Method — 给结构体装上行为
上一篇我们定义了结构体,它只有"数据"。Go 没有 class,但你可以给结构体(或任何自定义类型)绑定方法,让数据和行为走到一起。方法和普通函数几乎一样,唯一的区别是多了一个接收者(receiver)参数——它就是"当前对象"的占位,类似 Python 里的 self 或 JS 里的 this。理解了接收者,就理解了 Go 方法。
一、方法怎么定义
方法就是在 func 和方法名之间,多写一对括号、里面放一个接收者参数。比如 func (r Rect) Area() float64,这里的 r Rect 就是接收者,表示"这个方法属于 Rect 类型"。调用时用点号 r.Area(),和其它语言调用对象方法完全一样。接收者变量名通常用类型的第一个字母(比如 Rect 用 r、Person 用 p),这是 Go 社区的简写习惯。
二、值接收者 vs 指针接收者
这是 Go 方法最关键的一组概念。接收者有两种写法:写成 (r Rect) 叫值接收者,方法内部拿到的是原对象的一份拷贝,改它不影响外部;写成 (r *Rect) 叫指针接收者,方法内部直接操作原对象,改它真的会改外部。看下面这个对比,瞬间就懂。
package main
import "fmt"
type Rect struct {
W, H float64
}
// 值接收者:操作的是副本,不改原对象,适合"只读"方法
func (r Rect) Area() float64 {
return r.W * r.H
}
// 指针接收者:操作原对象本身,能真正修改字段
func (r *Rect) Scale(factor float64) {
r.W *= factor
r.H *= factor
}
func main() {
r := Rect{W: 3, H: 4}
fmt.Println(r.Area()) // 12
r.Scale(2) // Go 自动取址,等价于 (&r).Scale(2)
fmt.Println(r.Area()) // 48
}仔细看 r.Scale(2) 这一行:虽然 r 是个值不是指针,但 Go 编译器自动帮你取址,等价于 (&r).Scale(2)。反过来,如果方法定义成值接收者、却用指针调用,Go 也会自动解引用。这种自动转换只对"可寻址"的变量有效(比如 r 这种变量可以),字面量或 map 元素不可寻址,就只能手动处理。
三、什么时候用指针接收者
新手最常纠结的就是该用值还是指针。记住三条原则,基本不会错:
- 方法要修改原对象——必须用指针接收者,否则改的只是副本。下面这段对比代码就是反面教材。
- 结构体比较大——值接收者每次调用都拷贝整个对象,开销大,用指针只传一个地址。
- 保持一致性——如果某个类型有任何一个方法用了指针接收者,建议所有方法都用指针,避免在"方法集"和接口实现上踩坑。
反过来,如果方法只是"读"不修改、结构体又很小(比如一个坐标点),用值接收者更直观,也不用担心意外的副作用。
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 错误示范:值接收者改的是副本,原对象纹丝不动
func (p Person) BirthdayWrong() {
p.Age++
}
// 正确写法:指针接收者,真正修改原对象
func (p *Person) Birthday() {
p.Age++
}
func main() {
p := Person{Name: "小明", Age: 20}
p.BirthdayWrong()
fmt.Println(p.Age) // 还是 20(改的是副本)
p.Birthday()
fmt.Println(p.Age) // 21(改的是原对象)
}这段代码把"值 vs 指针"的差异讲得非常直白:BirthdayWrong 改的是副本,原对象纹丝不动;Birthday 用指针,原对象的 Age 真的变了。这是面试和实战都超高频的考点。
四、方法集
每个类型都有一个"方法集"——它能调用的所有方法的集合。规则很明确:值类型 *T 的方法集只包含值接收者的方法;而指针类型 *T 的方法集包含值接收者 + 指针接收者的全部方法。这条规则在讲接口(下一篇)时会变得非常重要——因为一个类型要实现某个接口,必须把接口里声明的所有方法都补齐,而指针和值的方法集不同,可能"值没实现、指针实现了"。现在先记住结论即可。
五、内嵌字段的方法会被提升
上一章讲结构体时说过"匿名嵌入会提升字段",这里还有个更实用的延伸:内嵌字段的方法同样会被提升。也就是说,外层结构体可以直接调用内嵌结构体的方法,就像那些方法是它自己的一样。这让你可以"组装"出功能丰富的类型,而不需要写一长串转调代码。
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Eat() {
fmt.Println(a.Name, "在吃东西")
}
func (a *Animal) Rename(newName string) {
a.Name = newName
}
// Dog 匿名嵌入 Animal,自动获得 Animal 的方法
type Dog struct {
Animal
}
func main() {
d := Dog{Animal{Name: "旺财"}}
// Animal 的方法被"提升",可以直接在外层调用
d.Eat() // 旺财 在吃东西
d.Rename("小黑") // 指针接收者方法同样会被提升
d.Eat() // 小黑 在吃东西
}上面 Dog 没有自己定义任何方法,但因为嵌入了 Animal,它直接获得了 Eat 和 Rename。注意这里既有值接收者方法(Eat)、又有指针接收者方法(Rename),提升规则对两者一视同仁——外层值或指针都能调用到。这是 Go 实现"复用"最顺手的方式。
六、方法也能给非结构体类型加
方法不只能绑给 struct,任何本包内定义的类型都能绑方法。比如你定义 type MyInt int,就能给它写 func (m MyInt) IsEven() bool。但注意:不能直接给 int、string 这些来自别的包的基础类型加方法——那会破坏类型定义的边界。需要时用 type 起一个新名字即可。
小结
- 方法 = 函数 + 接收者,接收者相当于
this/self。 - 值接收者改副本、指针接收者改原对象;要修改就用指针。
- Go 自动取址/解引用,但仅对可寻址的变量。
- 内嵌字段的方法会被提升,实现优雅的代码复用。
方法让类型有了"行为",下一篇 Go 接口 Interface 讲如何用接口把不同类型的行为统一起来。上一篇 Go 结构体。