bigywater.tech
Go · 25

方法集:值接收者与指针接收者,谁能实现接口

“值接收者还是指针接收者”不只是风格问题,它决定了哪个类型能实现哪个接口,以及调用时修改会不会生效。这篇用 reflect 列出方法集,用编译器的原样报错确认规则,再看方法值、方法表达式与 nil 接收者的行为。

方法集
T 的方法集只有值接收者的方法;*T 的包含全部
不可寻址
map 元素与函数返回值不能调用指针接收者的方法
绑定时拷贝
值接收者的方法值在绑定时拷贝接收者

方法集与接口

实测 · 本机执行输出
  源码(省略 main 与辅助):
      1 | type Counter struct{ n int }
      2 | 
      3 | func (c Counter) Value() int { return c.n }
      4 | func (c *Counter) Inc()      { c.n++ }
      5 | func (c Counter) IncLost()   { c.n++ }
  Counter 的方法集(reflect):IncLost,Value;*Counter 的方法集:Inc,IncLost,IncNilSafe,Value
  c.Inc() 两次、c.IncLost() 一次之后 c.n = 2(值接收者的修改作用在副本上,丢了)

规范里的规则:类型 T 的方法集由接收者是 T 的方法组成,*T 的方法集包含接收者是 T 或 *T 的方法。实验里 reflect 列出的两个集合正是这样。所以 Counter{} 不能赋给要求 Inc 的接口,必须用 &Counter{}。

编译器怎么说

实测 · 本机执行输出
  main.go:16:13: cannot use Counter{} (value of struct type Counter) as I value in variable declaration: Counter does not implement I (method Inc has pointer receiver)
  main.go:18:9: cannot call pointer method Inc on Counter
  main.go:19:8: cannot call pointer method Inc on Counter

报错信息直接指出了原因:method Inc has pointer receiver。直接对变量调用 c.Inc() 之所以可以,是因为编译器自动取了 (&c).Inc()——这要求 c 可寻址;map 元素和函数返回值不可寻址,所以不行。

方法值与方法表达式

实测 · 本机执行输出
  fv := c2.Value(值接收者,绑定时拷贝了 c2);之后 c2.n = 50:fv() = 1;fp := c2.Inc(指针接收者,绑定的是 &c2);fp() 之后 c2.n = 51
  方法表达式:Counter.Value 的类型是 func(main.Counter) int;(*Counter).Inc 的类型是 func(*main.Counter);pe(&cc) 之后 me(cc) = 8

c2.Value 作为方法值时,值接收者会在绑定的那一刻被拷贝,之后 c2 再变化不影响它;指针接收者绑定的是地址,所以能看到后来的修改。方法表达式 T.Method 把接收者变成第一个参数,类型分别是 func(main.Counter) int 与 func(*main.Counter)。

nil 接收者

实测 · 本机执行输出
  np.Value()(值接收者)           panic:runtime error: invalid memory address or nil pointer dereference
  正常返回,np == nil 时被方法自己处理了

值接收者的方法要先解引用指针来拷贝接收者,所以对 nil 指针调用会 panic;指针接收者的方法可以收到 nil,是否安全看方法体自己的判断(上一节“nil 接口陷阱”里的 NilSafe 就是这样)。一个类型里混用两种接收者是允许的,不会有编译错误,这是否合适是风格问题,本文没有验证 go vet 或其它工具是否提示。

分步演示

方法集
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。方法集用 reflect.Type.NumMethod / Method 列出;编译错误是 go build 的原样输出
    • 没有验证:嵌入结构体时方法集的提升规则(见下一篇)、泛型类型的方法集、接口里存放值时方法调用的拷贝开销、go vet 或 staticcheck 对混用接收者的提示
    京ICP备2026067502号-1京公网安备11010502063487号