1、写在前面
运行下面一段代码:
package main
type Cat int
type Animal interface {
Eat()
}
//go:noinline
func (cat Cat) Eat() {}
var (
c *Cat
animal Animal = c
)
func main() {
animal.Eat()
}
预计得到一个空指针异常,却得到另一个报错:
panic: value method main.Cat.Eat called using nil *Cat pointer
本篇文章浅浅总结一下关于这个报错的原理和关于接口包装方法的学习心得。
2、接口的包装方法
包装方法是啥?在上面的场景中,编译器会为接收者为值类型的方法生成接收者为指针类型的方法,也就是所谓的“包装方法”。
下面来验证一下,执行命令:
go tool compile main.go // 生成 main.o 文件
go tool nm main.o | grep T // 查看文件中实现的函数
执行结果:
1ecf T "".(*Cat).Eat
1e5f T "".Animal.Eat
1981 T "".Cat.Eat
可以看到一共实现了三个Eat()方法,第二个是Animal接口声明的Eat()方法,第三个是Cat的Eat()方法,接收者类型是Cat,而第一个接收者类型为*Cat的Eat()方法在源码中并没有定义,就是编译器生成的包装方法。
至于生成包装方法的原因,可以参考这篇博客:GoLang之包装方法系列一,这里大概总结一下就是:
对于接口来讲,在调用指针接收者方法时,传递地址是非常方便的,也不用关心数据的具体类型,地址的大小总是一致的。
假如通过接口调用值接收者方法,就需要通过接口中的data指针把数据的值拷贝到栈上,由于编译阶段不能确定接口背后的具体类型,所以编译器不能生成相关的指令来完成拷贝,所以说,接口是不能直接使用值接收者方法的,这就是编译器生成包装方法的根本原因。
3、寻找真相
根据推测,例子调用的animal.Eat(),不是接收者为值类型的Cat.Eat(),应该是包装方法(*Cat).Eat(),通过执行go tool compile -S main.go
看下main()方法汇编:
"".main STEXT size=57 args=0x0 locals=0x10 funcid=0x0
0x0000 00000 (main.go:17) TEXT "".main(SB), ABIInternal, $16-0
0x0000 00000 (main.go:17) CMPQ SP, 16(R14)
0x0004 00004 (main.go:17) PCDATA $0, $-2
0x0004 00004 (main.go:17) JLS 50
0x0006 00006 (main.go:17) PCDATA $0, $-1
0x0006 00006 (main.go:17) SUBQ $16, SP
0x000a 00010 (main.go:17) MOVQ BP, 8(SP)
0x000f 00015 (main.go:17) LEAQ 8(SP), BP
0x0014 00020 (main.go:17) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:17) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:18) MOVQ "".animal(SB), CX
0x001b 00027 (main.go:18) MOVQ "".animal+8(SB), AX
0x0022 00034 (main.go:18) MOVQ 24(CX), CX
0x0026 00038 (main.go:18) PCDATA $1, $0
0x0026 00038 (main.go:18) CALL CX
0x0028 00040 (main.go:19) MOVQ 8(SP), BP
0x002d 00045 (main.go:19) ADDQ $16, SP
0x0031 00049 (main.go:19) RET
0x0032 00050 (main.go:19) NOP
0x0032 00050 (main.go:17) PCDATA $1, $-1
0x0032 00050 (main.go:17) PCDATA $0, $-2
0x0032 00050 (main.go:17) CALL runtime.morestack_noctxt(SB)
0x0037 00055 (main.go:17) PCDATA $0, $-1
0x0037 00055 (main.go:17) JMP 0
奇怪,在汇编中并没有看到有调用(*Cat).Eat()方法,只有这句call调用:
0x0026 00038 (main.go:18) CALL CX
在这之上做了这些操作:
0x0014 00020 (main.go:18) MOVQ "".animal(SB), CX
0x001b 00027 (main.go:18) MOVQ "".animal+8(SB), AX
0x0022 00034 (main.go:18) MOVQ 24(CX), CX
首先是把animal和animal+8的位置分别存在CX和AX寄存器上,然后把24(CX)存到CX寄存器上。animal是iface接口类型,iface的底层结构如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
24(CX)其实就是itab中的fun数组开头位置,上面对CX的call调用其实就是调用func数组中的第一个方法,在上面场景中就是(*Cat).Eat()。在把对象赋值给接口的时候,itab会保存对象的实际类型,该类型下和接口方法对应的指针类型的方法(包括生成的包装方法),会以字典序排列在itab.func数组中。 注意itab.func数组大小只有1,这其实没有关系,多出来的方法会紧紧靠在func[1]的后面。
下面用例子进行验证,把这个func数组中所有方法都打印出来:
package main
import (
"runtime"
"unsafe"
)
type Cat int
type Animal interface {
Eat()
Run()
}
//go:noinline
func (cat Cat) Eat() {}
//go:noinline
func (cat Cat) Run() {}
//go:noinline
func (cat Cat) Sleep() {}
var (
c Cat = 1
animal Animal = c
)
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter unsafe.Pointer
_type unsafe.Pointer
hash uint32
_ [4]byte
fun [3]uintptr
}
func main() {
inter :=(*iface)(unsafe.Pointer(&animal))
t := inter.tab
println(runtime.FuncForPC(t.fun[0]).Name())
println(runtime.FuncForPC(t.fun[1]).Name())
println(runtime.FuncForPC(t.fun[2]).Name())
c.Sleep()
animal.Eat()
animal.Run()
}
打印结果:
main.(*Cat).Eat
main.(*Cat).Run
例子中Animal接口有Eat()和Run()方法,Cat类型实现了值接收者的Eat(),Run()和Sleep()方法,打印结果显示func数组保存了(*Cat).Eat和(*Cat).Run,没有(*Cat).Sleep。
既然知道CALL CX
其实是调用(*Cat).Eat这个包装方法,让我们看看汇编:
"".(*Cat).Eat STEXT dupok size=98 args=0x8 locals=0x10 funcid=0x16
0x0000 00000 (<autogenerated>:1) TEXT "".(*Cat).Eat(SB), DUPOK|WRAPPER|ABIInternal, $16-8
0x0000 00000 (<autogenerated>:1) CMPQ SP, 16(R14)
0x0004 00004 (<autogenerated>:1) PCDATA $0, $-2
0x0004 00004 (<autogenerated>:1) JLS 63
0x0006 00006 (<autogenerated>:1) PCDATA $0, $-1
0x0006 00006 (<autogenerated>:1) SUBQ $16, SP
0x000a 00010 (<autogenerated>:1) MOVQ BP, 8(SP)
0x000f 00015 (<autogenerated>:1) LEAQ 8(SP), BP
0x0014 00020 (<autogenerated>:1) MOVQ 32(R14), R12
0x0018 00024 (<autogenerated>:1) TESTQ R12, R12
0x001b 00027 (<autogenerated>:1) JNE 80
0x001d 00029 (<autogenerated>:1) NOP
0x001d 00029 (<autogenerated>:1) FUNCDATA $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
0x001d 00029 (<autogenerated>:1) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x001d 00029 (<autogenerated>:1) FUNCDATA $5, "".(*Cat).Eat.arginfo1(SB)
0x001d 00029 (<autogenerated>:1) MOVQ AX, ""..this+24(SP)
0x0022 00034 (<autogenerated>:1) TESTQ AX, AX
0x0025 00037 (<autogenerated>:1) JEQ 57
0x0027 00039 (<autogenerated>:1) MOVQ (AX), AX
0x002a 00042 (<autogenerated>:1) PCDATA $1, $1
0x002a 00042 (<autogenerated>:1) CALL "".Cat.Eat(SB)
0x002f 00047 (<autogenerated>:1) MOVQ 8(SP), BP
0x0034 00052 (<autogenerated>:1) ADDQ $16, SP
0x0038 00056 (<autogenerated>:1) RET
0x0039 00057 (<autogenerated>:1) CALL runtime.panicwrap(SB)
0x003e 00062 (<autogenerated>:1) XCHGL AX, AX
0x003f 00063 (<autogenerated>:1) NOP
0x003f 00063 (<autogenerated>:1) PCDATA $1, $-1
0x003f 00063 (<autogenerated>:1) PCDATA $0, $-2
0x003f 00063 (<autogenerated>:1) MOVQ AX, 8(SP)
0x0044 00068 (<autogenerated>:1) CALL runtime.morestack_noctxt(SB)
0x0049 00073 (<autogenerated>:1) MOVQ 8(SP), AX
0x004e 00078 (<autogenerated>:1) PCDATA $0, $-1
0x004e 00078 (<autogenerated>:1) JMP 0
0x0050 00080 (<autogenerated>:1) LEAQ 24(SP), R13
0x0055 00085 (<autogenerated>:1) CMPQ (R12), R13
0x0059 00089 (<autogenerated>:1) JNE 29
0x005b 00091 (<autogenerated>:1) MOVQ SP, (R12)
0x005f 00095 (<autogenerated>:1) NOP
0x0060 00096 (<autogenerated>:1) JMP 29
原来包装方法内部实现其实还是调用原本值类型的方法的:
0x002a 00042 (<autogenerated>:1) CALL "".Cat.Eat(SB)
上面例子中的报错应该就是这个panicwrap引起的:
0x0039 00057 (<autogenerated>:1) CALL runtime.panicwrap(SB)
panicwrap实现:
// panicwrap generates a panic for a call to a wrapped value method
// with a nil pointer receiver.
//
// It is called from the generated wrapper code.
func panicwrap() {
// 省略...
panic(plainError("value method " + pkg + "." + typ + "." + meth + " called using nil *" + typ + " pointer"))
}
这个panicwrap的检测是在生成包装方法的时候构造了个if语句节点塞进方法体的:
func methodWrapper(rcvr *types.Type, method *types.Field) *obj.LSym {
// ...
// generate nil pointer check for better error
if rcvr.IsPtr() && rcvr.Elem() == methodrcvr {
// generating wrapper from *T to T.
n := ir.NewIfStmt(base.Pos, nil, nil, nil)
n.Cond = ir.NewBinaryExpr(base.Pos, ir.OEQ, nthis, typecheck.NodNil())
call := ir.NewCallExpr(base.Pos, ir.OCALL, typecheck.LookupRuntime("panicwrap"), nil)
n.Body = []ir.Node{call}
fn.Body.Append(n)
// ...
}
参考: