Go 方法集与接收者
解释值接收者和指针接收者如何决定方法集、可调用方法与接口满足关系。
[!info] 关联笔记
Go 方法集(Method Set)与接收者
[!abstract] 一句话理解 方法集(Method Set)决定一个类型“拥有哪些方法”;接口是否满足,只看方法集,不看编译器是否能自动帮你取地址。
这个概念为什么出现
很多 Go 初学者都会遇到这种情况:
var c Counter
c.Add() // 可以调用
但是:
var adder Adder = c
却报错:
Counter does not implement Adder
明明 c.Add() 能调用,为什么却不能实现接口?
答案就是:
[!important]
调用规则(Call Rule) ≠ 方法集(Method Set)
编译器允许自动取地址调用方法,但不会修改类型的方法集。
这是 Go 中最容易混淆的地方。
Receiver(接收者)
Go 的方法其实只是一个普通函数,多了一个特殊参数:
func (c Counter) Value() {}
等价理解为:
func Value(c Counter)
如果写成:
func (c *Counter) Add() {}
等价理解为:
func Add(c *Counter)
因此:
值接收者:
Receiver 是 T
指针接收者:
Receiver 是 *T
Method Set(方法集)
对于一个定义类型:
type T struct{}
Go 规定:
[!tip] Method Set 规则
T 的方法集
仅包含:
func (T) ...
即:
T
└── Value Receiver
*T 的方法集
包含:
func (T)
func (*T)
即:
*T
├── Value Receiver
└── Pointer Receiver
可以记住一句口诀:
指针拥有全部;值只有自己的。
为什么 *T 能调用值方法?
假设:
func (c Counter) Value()
调用:
p := &c
p.Value()
实际上编译器会自动解引用:
(*p).Value()
所以:
*T
↓ 自动解引用
T.Value()
因此:
指针既可以调用值方法,也可以调用指针方法。
为什么 T 可以调用指针方法?
如果:
func (c *Counter) Add()
而:
var c Counter
c.Add()
编译器会自动改写为:
(&c).Add()
但是有一个条件:
[!warning]
变量必须是可寻址(addressable)的。
例如:
Counter(1).Add()
编译失败。
因为:
Counter(1)
没有地址。
调用规则(Call Rule)
真正发生的是:
c.Add()
↓
(&c).Add()
这是:
语法糖(Syntax Sugar)
而不是:
Method Set 发生变化
所以千万不要混为一谈。
Method Set 与接口
假设:
type Counter int
func (c Counter) Value() int
func (c *Counter) Add()
接口:
type Valuer interface {
Value() int
}
判断:
Counter
└── Value()
✔ 满足
*Counter
├── Value()
└── Add()
✔ 满足
如果接口变成:
type Adder interface {
Add()
}
则:
Counter
└── (没有 Add)
✘ 不满足
*Counter
├── Value()
└── Add()
✔ 满足
所以:
var a Adder = Counter(1)
错误。
必须:
var a Adder = &Counter{}
完整示例(业务场景)
场景:订单浏览次数计数器
商品详情页要统计“被打开次数”:
Value():只读当前次数 → 适合值接收者Add():就地 +1 → 必须指针接收者,否则改的是副本
页面代码里 c.Add() 往往能编译(编译器自动取址),
但把 c(值)赋给 Adder 接口会失败——方法集不含指针方法。
这就是“能调用 ≠ 能赋值给接口”的坑。
package main
import "fmt"
// ViewCounter 模拟“商品浏览次数”。
type ViewCounter int
// Value:只读,值接收者即可。
func (c ViewCounter) Value() int {
return int(c)
}
// Add:要改接收者,必须用指针接收者。
func (c *ViewCounter) Add() {
*c++
}
// 只读能力
type Valuer interface {
Value() int
}
// 可递增能力(方法集要求 *ViewCounter)
type Adder interface {
Add()
}
func main() {
var c ViewCounter // 0
// 调用规则:变量 c 可寻址,编译器自动写成 (&c).Add()
c.Add()
fmt.Println("after direct Add:", c.Value()) // 1
// 值 c 的方法集包含 Value → 可赋给 Valuer
var v Valuer = c
fmt.Println("via Valuer:", v.Value()) // 1
// 值 c 的方法集不包含 Add → 不能 var a Adder = c(编译错误)
// 必须用指针,*ViewCounter 方法集才有 Add
var a Adder = &c
a.Add()
fmt.Println("after interface Add:", c) // 2
}
建议运行:
go run .
期望输出:
after direct Add: 1
via Valuer: 1
after interface Add: 2
结合场景再看三句话
c.Add()能调用,靠的是调用规则自动取址,不是值类型拥有Add。- 接口赋值只看方法集:
ViewCounter不满足Adder,*ViewCounter才满足。 - 会改接收者 → 指针接收者;只读可用值接收者。
Receiver 应该如何选择?
[!tip] 推荐原则
使用值接收者
适用于:
- 小对象
- 不修改状态
- 不可变语义
- 类似
time.Time
例如:
time.Time
使用指针接收者
适用于:
- 修改对象
- 对象较大
- 包含锁(sync.Mutex)
- 避免复制
例如:
bytes.Buffer
保持一致
同一个类型的方法:
最好统一全部使用值接收者,
或者全部使用指针接收者。
避免:
func (T) A()
func (*T) B()
导致接口实现变得难以判断。
易错点
[!warning] 1. 能调用 ≠ 有这个方法
c.Add()
只是自动取址。
并不是:
Counter
拥有:
Add()
[!warning] 2. 接口判断只看 Method Set
不会因为:
c.Add()
能够调用,
接口就认为:
Counter
实现了:
Add()
[!warning] 3. 临时值不能自动取址
例如:
Counter(1).Add()
错误。
因为:
Counter(1)
没有地址。
总结
[!summary]
可以记住四句话:
- T 只有值方法。
- *T 拥有值方法和指针方法。
- 接口判断只看 Method Set。
- 自动取址只是调用语法糖,不会改变 Method Set。
自测题
概念题
T与*T的方法集分别包含什么?- 为什么
c.Add()能调用,但var a Adder = c可能失败? - 接口满足检查看调用语法糖还是方法集?
代码推理题
type Counter int
func (c *Counter) Inc() { *c++ }
type Incable interface{ Inc() }
func main() {
var c Counter
c.Inc() // ?
// var i Incable = c // ?
var j Incable = &c // ?
_ = j
}
哪些行能编译?为什么?
参考答案
展开
T:仅值接收者方法;*T:值接收者 + 指针接收者方法。- 调用可自动取址;赋值给接口只看方法集,
T不含指针方法。 - 只看方法集。
代码:c.Inc()可(取址调用);var i Incable = c不可;&c可以。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Method sets | 规范 | 方法集定义 |
| Code Review Comments — Receiver Type | Wiki | 接收者选择惯例 |
| Effective Go — Methods | 官方 | 方法风格 |
| go-structs-and-methods · go-interfaces · go-interface-values-and-nil | 本库 | 相邻主题 |