p现在是个指针,其余代码不需要任何改动,程序依旧可以正常编译执行。对应的汇编是这样的画风(当然得关闭优化):
p.sayHello() MOVQ AX, 0(SP) CALL main.(*Person).sayHello(SB)对比一下非指针版本:
p.sayHello() LEAQ 0x8(SP), AX MOVQ AX, 0(SP) CALL main.(*Person).sayHello(SB)与其说是指针自动解引用,倒不如说是非指针版本先求出了对象的实际地址,随后传入了这个地址作为方法的接收器调用了方法。这也没什么好奇怪的,因为我们的方法是指针接收器:P。
如果把接收器换成值类型接收器:
p.sayHello() TESTB AL, 0(AX) MOVQ 0x40(SP), AX MOVQ 0x48(SP), CX MOVQ 0x50(SP), DX MOVQ AX, 0x28(SP) MOVQ CX, 0x30(SP) MOVQ DX, 0x38(SP) MOVQ AX, 0(SP) MOVQ CX, 0x8(SP) MOVQ DX, 0x10(SP) CALL main.Person.sayHello(SB)作为对比:
p.sayHello() MOVQ AX, 0(SP) MOVQ $0xa, 0x8(SP) MOVQ $0x64, 0x10(SP) CALL main.Person.sayHello(SB)这时候golang就是先检查指针随后解引用了。同时要注意,这里的方法调用是已经在编译期确定了的。
指向interface的指针铺垫了这么久,终于该进入正题了。不过在此之前还有一点小小的预备知识需要提一下:
A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. --- go language spec
换而言之,只要是能取地址的类型就有对应的指针类型,比较巧的是在golang里引用类型是可以取地址的,包括interface。
有了这些铺垫,现在我们可以看一下我们的说唱歌手程序了:
package main import "fmt" type Rapper interface { Rap() string } type Dean struct {} func (_ Dean) Rap() string { return "Im a rapper" } func doRap(p *Rapper) { fmt.Println(p.Rap()) } func main(){ i := new(Rapper) *i = Dean{} fmt.Println(i.Rap()) doRap(i) }问题来了,小青年Dean能圆自己的说唱梦么?
很遗憾,编译器给出了反对意见:
# command-line-arguments ./rapper.go:16:18: p.Rap undefined (type *Rapper is pointer to interface, not interface) ./rapper.go:22:18: i.Rap undefined (type *Rapper is pointer to interface, not interface)也许type *XXX is pointer to interface, not interface这个错误你并不陌生,你曾经也犯过用指针指向interface的错误,经过一番搜索后你找到了一篇教程,或者是博客,有或者是随便什么地方的资料,他们都会告诉你不应该用指针去指向接口,接口本身是引用类型无需再用指针去引用。
其实他们只说对了一半,事实上只要把i和p改成接口类型就可以正常编译运行了。没说对的一半是指针可以指向接口,也可以使用接口的方法,但是要绕些弯路(当然,用指针引用接口通常是多此一举,所以听从经验之谈也没什么不好的):
func doRap(p *Rapper) { fmt.Println((*p).Rap()) } func main(){ i := new(Rapper) *i = Dean{} fmt.Println((*i).Rap()) doRap(i) } go run rapper.go Im a rapper Im a rapper神奇的一幕出现了,程序不仅没报错而且运行得很正常。但是这和golang对指针的自动解引用有什么区别呢?明明看起来都一样但就是第一种方案会报
找不到Rap方法?
为了方便观察,我们把调用语句单独抽出来,然后查看未优化过的汇编码:
s := (*p).Rap() 0x498ee1 488b842488000000 MOVQ 0x88(SP), AX 0x498ee9 8400 TESTB AL, 0(AX) 0x498eeb 488b08 MOVQ 0(AX), CX 0x498eee 8401 TESTB AL, 0(CX) 0x498ef0 488b4008 MOVQ 0x8(AX), AX 0x498ef4 488b4918 MOVQ 0x18(CX), CX 0x498ef8 48890424 MOVQ AX, 0(SP) 0x498efc ffd1 CALL CX抛开手工解引用的部分,后6行其实和直接使用interface进行动态查询是一样的。真正的问题其实出在自动解引用上:
p.sayHello() TESTB AL, 0(AX) MOVQ 0x40(SP), AX MOVQ 0x48(SP), CX MOVQ 0x50(SP), DX MOVQ AX, 0x28(SP) MOVQ CX, 0x30(SP) MOVQ DX, 0x38(SP) MOVQ AX, 0(SP) MOVQ CX, 0x8(SP) MOVQ DX, 0x10(SP) CALL main.Person.sayHello(SB)不同之处就在于这个CALL上,自动解引用时的CALL其实是把指针指向的内容视作_普通类型_,因此会去静态查找方法进行调用,而指向的内容是interface的时候,编译器会去interface本身的数据结构上去查找有没有Rap这个方法,答案显然是没有,所以爆了p.Rap undefined错误。