方法集是 Go 接口编程中最易踩坑的概念。看懂它,接口逻辑就通了。
一、什么是方法集?
方法集(Method Set)是类型(值 T 或指针 *T)能作为接收者调用的全部方法名单。
它的唯一作用:判断一个类型是否实现了某个接口。
二、核心规则
| 接收者类型 | 包含 T 的值方法 | 包含 *T 的指针方法 |
|---|---|---|
| T | ✅ | ❌ |
| *T | ✅ | ✅ |
一句话:*T 是 T 的超集,大圈包小圈。
三、为什么出这个设计?
3.1 指针 → 值是确定的
type Cat struct{ name string }
func (c Cat) Eat() {} // 值方法
var p *Cat = &Cat{"小花"}
p.Eat() // ✅ 编译器自动解引用 -> (*p).Eat()
// Cat 从 *Cat 是 100% 可及的
3.2 值 → 指针不一定
func (c *Cat) Chase() {} // 指针方法
Cat{} // ← 临时值,不存在可用地址
Cat{}.Chase() // ❌ 不可寻址 → 方法集中不包含 Chase
不能保证所有值都能取地址,所以 T 的方法集不包含指针方法。
四、方法集和接口(核心场景)
type Dog struct{ Name string }
func (d Dog) Bark() { fmt.Println("汪汪") } // 值方法
func (d *Dog) Run() { fmt.Println("跑10米") } // 指针方法
type Runner interface { Run() }
var r Runner
r = Dog{} // ❌ Dog 的方法集中没有 Run
r = &Dog{} // ✅ *Dog 的方法集中有 Bark + Run
接口只看值本身的方法集是否完整,不看编译器语法糖有没有帮你取地址。
五、记忆口诀:编译能调 ≠ 方法集里有
| 操作 | 编译是否通过 | 属于方法集? |
|---|---|---|
| d.Run() | ✅(编译器自动 (&d).Run()) | ❌ Run 不在 T 的方法集 |
| p.Bark() | ✅(编译器自动 (*p).Bark()) | ✅ *T 的方法集合包含一切 |
| Dog{}.Run() | ❌ 编译报错 | ❌ 不可寻址,更勿论方法集 |
六、选择值还是指针接收者?
| 场景 | 值接收者 | 指针接收者 |
|---|---|---|
| 不改字段 | ✅ | ✅ |
| 改字段 | ❌ 改副本 | ✅ 改原值 |
| 大结构体避免拷贝 | ❌ 拷贝开销大 | ✅ 只传指针 |
| 接口用值实例化 | T 满足就行 | 不纠结 |
推荐: 默认从指针接收器开始设计,未来扩展接口不被动。
七、运行验证
go doc type.TypeName # 查看类型的方法集
go vet main.go # 静态检查接口装量
import "reflect"
// 另法:用反射列出一个类型的所有方法
t := reflect.TypeOf((*Dog)(nil)).Elem()
for i:=0; i<t.NumMethod(); i++ {
fmt.Println(t.Method(i).Name)
}
八、一句话总结
| 方法集 | |
|---|---|
| 值类型 T | 只含在 T 上面定义的方法 |
| 指针类型 *T | 含在 T 和 *T 上面定义的所有方法 |
接口不变只看方法集,不看编译器的语法糖帮你自动传参的。