《关于golang值类型变量转换成接口类型之后无法调用接收者为指针的接口方法这件事》

Kesa...大约 6 分钟golangwhy

为何值类型的变量转换成接口变量之后无法调用接收者为指针的接口方法?

1. 接口和方法集

对于变量的类型和其接口方法的接收者类型,当其转换成接口后的方法调用情况如下:

变量类型\接口方法接收者类型指针
不可
指针

当尝试使用值类型的变量转换成接口,然后调用指针类型的方法:

type Duck interface {
	Quack()
}

type Cat struct {
	Name string
}

func (c *Cat) Quack() {}

func main() {
	var d Duck = Cat{Name: "cat"}
	d.Quack()
}

若编译文件,则会出现错误:

$ go tool compile main.go
main.go:14:15: cannot use Cat{} (value of type Cat) as Duck value in variable declaration: Cat does not implement Duck (method Quack has pointer receiver)

Cat并未实现Duck接口,方法Quack的接收者为指针类型。

2. Why

根本原因:Golang 的函数参数传递是 值传递,即函数的参数是原参数的一份拷贝。

对于变量的类型:

  1. 指针类型,转换成接口后,接口变量持有的是原变量的指针
    • 可直接调用接收者为指针类型的接口方法
    • 可间接调用接收者为值类型(自动解引用)的接口方法
  2. 类型,转换成接口后,接口变量持有的是原变量的拷贝
    • 可直接调用接收者为值类型的接口方法
    • 不可调用接收者为指针类型的接口方法,无法获取原变量的指针,即接口持有的值类型变量无法寻址

上述的原因是从宏观层面上得出的结论,但是要想知道是如何发生拷贝的或者说为什么无法寻址,需要具体分析函数的执行情况。

3. Golang Assembly

The Go compiler outputs an abstract, portable form of assembly that doesn't actually map to any real hardware. The Go assembler then uses this pseudo-assembly output in order to generate concrete, machine-specific instructions for the targeted hardware.

Golang 的汇编是伪汇编代码,最终根据不同的机器指令生成具体的汇编代码。

Golang 采用 Plan 9 assemblersopen in new window,其语法和汇编有些许差异,比如:指令的计算方向和汇编相反,从左到右;MOVQ AX DI表示将 AX的内容移动至DI。

Golang 汇编中有四个伪寄存器:

  • FP: Frame pointer: arguments and locals.
  • PC: Program counter: jumps and branches.
  • SB: Static base pointer: global symbols.
  • SP: Stack pointer: the highest address within the local stack frame.

其中,需要关注的是SP表示当前栈帧的栈顶地址,SB用于表示基址,即在内存中的初始位置,例如:foo(SB) 是 foo 在内存中的地址。

4. How

想要知道为何不能调用指针类型的接口方法,需要分析两点:

  1. 变量到接口变量的类型转换
  2. 接口变量调用方法

指针类型 -> 接口

首先分析,变量为指针类型时,转换成接口后如何调用方法的。

以下面代码为例:

type Duck interface {
	Quack()
    Quack2()
}

type Cat struct {
	Name string
}

func (c *Cat) Quack() {}
func (c *Cat) Quack2() {}

func main() {
	var d Duck 
    d = &Cat{Name: "mycat"}
	d.Quack()
    d.Quack2()
}

上述代码中,值类型Cat实现了接口Duck

现将其编译成汇编,查看其执行流程:

$ go tool compile -S -N -l main.go > asm.txt
  • go tool compile -S -N -l: 编译文件,-N关闭编译器优化,-l关闭内联编译,-S输出为汇编格式

只需关注其类型转换和函数调用流程。

首先分析类型转换部分:

main.main STEXT size=171 args=0x0 locals=0x38 funcid=0x0 align=0x0
...
0x001e 00030 (D:\tmp\gotmp\main.go:17)	LEAQ	type:main.Cat(SB), AX
...
0x0025 00037 (D:\tmp\gotmp\main.go:17)	CALL	runtime.newobject(SB)
0x002a 00042 (D:\tmp\gotmp\main.go:17)	MOVQ	AX, main..autotmp_2+16(SP)
0x002f 00047 (D:\tmp\gotmp\main.go:17)	MOVQ	$5, 8(AX)
...
0x0044 00068 (D:\tmp\gotmp\main.go:17)	LEAQ	go:string."mycat"(SB), CX
0x004b 00075 (D:\tmp\gotmp\main.go:17)	MOVQ	CX, (AX)
...
0x0050 00080 (D:\tmp\gotmp\main.go:17)	MOVQ	AX, DI
...
0x0062 00098 (D:\tmp\gotmp\main.go:17)	MOVQ	main..autotmp_2+16(SP), AX
0x0067 00103 (D:\tmp\gotmp\main.go:17)	MOVQ	AX, main..autotmp_1+24(SP)
0x006c 00108 (D:\tmp\gotmp\main.go:17)	LEAQ	go:itab.*<unlinkable>.Cat,<unlinkable>.Duck(SB), CX
0x0073 00115 (D:\tmp\gotmp\main.go:17)	MOVQ	CX, main.d+32(SP)
0x0078 00120 (D:\tmp\gotmp\main.go:17)	MOVQ	AX, main.d+40(SP)
  • 首先调用runtime.newobjectopen in new window在堆上初始化类型Cat,并返回指针*Cat
  • *Cat指向的堆内存中写入数据,相当于&StringHeader{Data: &"mycat", Len: 5}
  • 然后初始化接口类型,相当于&iface{itab: ..., data: *Cat}

类型转换的栈内存如下图所示:

+-------------------+                              
|                   ------------+                  
|      *Cat         |           |                  
---------------------   40 SP   |                  
|                   |           |  +--------------+
|    *itav,Duck     |           |  |              |
---------------------   32 SP   |  |     5        |
|                   |           |  ----------------
|       ...         |           |  |              |
---------------------   24 SP   |---    "mycat"   |
|                   |              +--------------+
|       ...         |                              
---------------------   16 SP            Heap      
|                   |                              
|       ...         |                              
+-------------------+   SP                         

函数调用部分:

main.Duck.Quack STEXT dupok size=108 args=0x10 locals=0x10 funcid=0x15 align=0x0
	...
	0x001d 00029 (<autogenerated>:1)	MOVQ	AX, main..this+24(SP)
	0x0022 00034 (<autogenerated>:1)	MOVQ	BX, main..this+32(SP)
	0x0027 00039 (<autogenerated>:1)	TESTB	AL, (AX)
	0x0029 00041 (<autogenerated>:1)	MOVQ	24(AX), CX
	0x002d 00045 (<autogenerated>:1)	MOVQ	BX, AX
	0x0030 00048 (<autogenerated>:1)	PCDATA	$1, $1
	0x0030 00048 (<autogenerated>:1)	CALL	CX
	0x0032 00050 (<autogenerated>:1)	MOVQ	8(SP), BP
	0x0037 00055 (<autogenerated>:1)	ADDQ	$16, SP
	0x003b 00059 (<autogenerated>:1)	RET
	...
	
main.Duck.Quack2 STEXT dupok size=108 args=0x10 locals=0x10 funcid=0x15 align=0x0
	...
	0x001d 00029 (<autogenerated>:1)	MOVQ	AX, main..this+24(SP)
	0x0022 00034 (<autogenerated>:1)	MOVQ	BX, main..this+32(SP)
	0x0027 00039 (<autogenerated>:1)	TESTB	AL, (AX)
	0x0029 00041 (<autogenerated>:1)	MOVQ	32(AX), CX
	0x002d 00045 (<autogenerated>:1)	MOVQ	BX, AX
	0x0030 00048 (<autogenerated>:1)	PCDATA	$1, $1
	0x0030 00048 (<autogenerated>:1)	CALL	CX
	...
  • AX指向的是Duck变量的地址

  • 24(AX)Duck变量中函数指针数组的位置:

    type itab struct {
    	inter *interfacetype // 8 Byte
    	_type *_type		 // 8 Byte
    	hash  uint32 		 // 4 Byte
    	_     [4]byte		 // 4 Byte
    	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
    }
    
  • 24(AX):函数Quack()

  • 32(AX):函数Quack2()

值类型 -> 接口

若改为值类型,如代码所示:

type Duck interface {
	Quack()
}

type Cat struct {
	Name string
}

func (c Cat) Quack() {}

func main() {
	var d Duck
	d = Cat{Name: "mycat"}
	d.Quack()
}

编译后分析类型转换部分:

main.main STEXT size=156 args=0x0 locals=0x60 funcid=0x0 align=0x0
	...
	0x0024 00036 (D:\tmp\gotmp\main.go:15)	LEAQ	go:string."mycat"(SB), CX
	0x002b 00043 (D:\tmp\gotmp\main.go:15)	MOVQ	CX, main..autotmp_1+72(SP)
	0x0030 00048 (D:\tmp\gotmp\main.go:15)	MOVQ	$5, main..autotmp_1+80(SP)
	0x0039 00057 (D:\tmp\gotmp\main.go:15)	MOVQ	CX, main..autotmp_2+56(SP)
	0x003e 00062 (D:\tmp\gotmp\main.go:15)	MOVQ	$5, main..autotmp_2+64(SP)
	0x0047 00071 (D:\tmp\gotmp\main.go:15)	LEAQ	main..autotmp_2+56(SP), CX
	...
	0x004e 00078 (D:\tmp\gotmp\main.go:15)	MOVQ	main..autotmp_2+56(SP), AX
	0x0053 00083 (D:\tmp\gotmp\main.go:15)	MOVQ	AX, main..autotmp_3+40(SP)
	0x0058 00088 (D:\tmp\gotmp\main.go:15)	MOVQ	$5, main..autotmp_3+48(SP)
	...
	0x0066 00102 (D:\tmp\gotmp\main.go:15)	CALL	runtime.convTstring(SB)
	0x006b 00107 (D:\tmp\gotmp\main.go:15)	MOVQ	AX, main..autotmp_4+16(SP)
	0x0070 00112 (D:\tmp\gotmp\main.go:15)	LEAQ	go:itab.<unlinkable>.Cat,<unlinkable>.Duck(SB), CX
	0x0077 00119 (D:\tmp\gotmp\main.go:15)	MOVQ	CX, main.d+24(SP)
	0x007c 00124 (D:\tmp\gotmp\main.go:15)	MOVQ	AX, main.d+32(SP)
	...
  • 初始化变量,Cat{Name: "mycat"}, 此时初始化位置为,(上个程序&Cat{Name: "mycat"}初始化位置为
  • runtime.convTstringopen in new window将当前的字符串,即Cat.Name拷贝到上,返回新分配的内存地址(这个地址就是新的Cat变量的地址,因为Name是Cat的第一个字段,Cat的地址和Name是一样的)
  • 初始化接口变量,此时接口变量持有的是新的Cat变量,原变量的地址已无法获取

综上,因为值类型的变量在转换成接口类型时,发生了值的拷贝,接口类型持有的变量不再是原变量,导致无法获取原变量的地址。

Reference

  1. https://draveness.me/golangopen in new window
  2. https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/11.6.mdopen in new window
  3. https://go.dev/doc/asm#data-offsetsopen in new window
  4. https://cmc.gitbook.io/go-internals/chapter-i-go-assembly#pseudo-assemblyopen in new window
  5. https://9p.io/sys/doc/asm.htmlopen in new window
  6. https://textik.com/open in new window
  7. https://www.cnblogs.com/jiujuan/p/16555192.htmlopen in new window
  8. https://stuff.mit.edu/afs/sipb/project/golang/arch/amd64_deb40/doc/asm.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2