学完这篇你能分清 Go 里值接收者和指针接收者各自的方法集,知道为什么 var _ I = T{} 会编译不过在 &T{} 上却没问题,也能在写代码时一眼看出该用哪种接收者。
第一步:先看清两种接收者的行为差别
type Counter struct{ n int }
func (c Counter) IncVal() { c.n++ } // 收到的是副本
func (c *Counter) IncPtr() { c.n++ } // 收到的是本体地址
c := Counter{}
c.IncVal() // c.n 仍是 0
c.IncPtr() // c.n 变成 1
值接收者拿到的是一份拷贝,改它不影响原值;指针接收者能改到原值。这是所有后续问题的根源。
第二步:记住方法集规则(一张表就够)
方法集决定了一个类型能不能满足某个接口。规则只有两条:
| 类型 | 方法集包含 |
|---|
T | 只有值接收者方法 |
*T | 值接收者方法 + 指针接收者方法 |
也就是说,指针接收者方法只「挂」在 *T 上。接口实现检查严格按方法集来,不做任何通融。
第三步:接口赋值时不会帮你取地址
这是新手最常遇到的编译错误:
type Incer interface{ IncPtr() }
var c Counter
var i Incer = c // ❌ Counter does not implement Incer
var j Incer = &c // ✅ *Counter 才满足
注意:c.IncPtr() 这种直接调用是合法的,因为编译器会自动写成 (&c).IncPtr()。但接口赋值不会自动取地址,这一步必须你自己写 &。
反过来,*T 天然拥有全部方法,所以 &c 既能满足只含值方法的小接口,也能满足含指针方法的接口。
第四步:自动取地址的前提是「可寻址」
上面说的自动取地址只在操作数可寻址时成立。以下场景会直接编译失败:
m := map[string]Counter{}
m["a"].IncPtr() // ❌ cannot call pointer method on m["a"]
func newCounter() Counter { return Counter{} }
newCounter().IncPtr() // ❌ 返回值不可寻址
s := []Counter{{}}
s[0].IncPtr() // ✅ 切片元素可寻址
注意:map 元素不可寻址,函数返回值不可寻址。遇到这种情况,要么先赋给局部变量、改完再写回,要么干脆把容器类型改成 map[string]*Counter。
第五步:接口里存的是拷贝,还是指针
var s Shape = Circle{r: 1} // 存的是 Circle 的值拷贝
var p Shape = &Circle{r: 1} // 存的是 *Circle
从接口取回来做类型断言时,类型必须完全对上:s.(Circle) 和 p.(*Circle),写错就是断言失败。
注意:接口变量是否为 nil,看的是「类型 + 值」两部分。把一个值为 nil 的 *T 塞进接口,接口本身不是 nil,if i == nil 会是 false。这是另一类高频 bug。
第六步:嵌入类型也会影响方法集
type Base struct{}
func (b *Base) Hello() {}
type Derived struct{ Base }
var _ Speaker = &Derived{} // ✅ *Derived 提升到了 Hello
var _ Speaker = Derived{} // ❌ Derived 的方法集里没有 Hello
嵌入 Base(值)时,指针方法只能提升到 *Derived;嵌入 *Base 时,Derived 和 *Derived 都能拿到。排查「明明嵌入了却不满足接口」时,先看这一层。
第七步:实际怎么写
- 需要修改接收者、结构体带
sync.Mutex 等不可拷贝字段、结构体较大 → 指针接收者
- 小的不可变值(如
time.Time 风格的包装)→ 值接收者
- 同一个类型的所有方法保持一致,混用会让方法集变得难以预测
- 想用编译期断言检查接口实现,在包级别写
var _ MyInterface = (*MyType)(nil)
小结
- 方法集:
T 只有值方法,*T 有全部方法;接口实现严格按方法集判断
- 接口赋值不会自动取地址,
var i I = v 失败就该写 &v
- 自动取地址只在可寻址时生效,map 元素和函数返回值不行
- 接口存值还是存指针,决定类型断言写成
T 还是 *T;接口非 nil 不等于底层指针非 nil
- 嵌入
*Base 比嵌入 Base 的方法集更宽
- 用
var _ I = (*T)(nil) 在编译期锁住接口契约