“值接收者还是指针接收者”不只是风格问题,它决定了哪个类型能实现哪个接口,以及调用时修改会不会生效。这篇用 reflect 列出方法集,用编译器的原样报错确认规则,再看方法值、方法表达式与 nil 接收者的行为。
源码(省略 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)。
np.Value()(值接收者) panic:runtime error: invalid memory address or nil pointer dereference 正常返回,np == nil 时被方法自己处理了
值接收者的方法要先解引用指针来拷贝接收者,所以对 nil 指针调用会 panic;指针接收者的方法可以收到 nil,是否安全看方法体自己的判断(上一节“nil 接口陷阱”里的 NilSafe 就是这样)。一个类型里混用两种接收者是允许的,不会有编译错误,这是否合适是风格问题,本文没有验证 go vet 或其它工具是否提示。