Golang 内部机制 第一部分:自动生成函数(以及如何摆脱它们)

也许你像我们一样,在 MinIO,时不时会在你的 Golang 调用栈中遇到 “自动生成” 的函数,并想知道它们究竟是什么?

前几天我们遇到了一个案例,调用栈显示了类似以下内容:

   cmd.retryStorage.ListDir(0x12847c0, 0xc420402e70, 0x1, ...)
 minio/cmd/retry-storage.go:183 +0x72

cmd.(*retryStorage).ListDir(0xc4201624b0, 0xdf3b5f, 0xe, 0x25, ...)
                           :932 +0xaf

       cmd.cleanupDir.func1(0xf18f1ec540, 0x25, 0x24, 0xde7eb5)
minio/cmd/object-api-com.go:215 +0xe1
                                                                   
             cmd.cleanupDir(0x1284860, 0xc4201624b0, 0xdf3b5f, ...)
minio/cmd/object-api-com.go:231 +0x1d1

正如你所看到的,第二个函数看起来与第一个函数很像(值接收器,在我们的 MinIO 代码 minio/cmd/retry-storage.go:183 中定义),但是它接受指针接收器。而且可疑的是,第二个函数只显示了行号,而没有显示源代码的文件名。

创建此调用栈的代码源于以下函数,其中声明了一个(递归)函数,该函数依次调用 storage.ListDirstorage 被传递到 cleanupDir 中。

// Cleanup a directory recursively.
func cleanupDir(storage StorageAPI, volume, dirPath string) error {
	var delFunc func(string) error
	// Function to delete entries recursively.
	delFunc = func(entryPath string) error {

		// List directories
		entries, err := storage.ListDir(volume, entryPath)
		if err != nil { 
			return err
		}

		// Recurse and delete all other entries
		for _, entry := range entries {
			err = delFunc(pathJoin(entryPath, entry))
			if err != nil {
				return err
			}
		}
		return nil
	}
	return delFunc(retainSlash(pathJoin(dirPath)))
}

查看上面的代码,你会认为 storage.ListDir(值接收器)会被立即调用,而无需调用指针接收器版本(cmd.(*retryStorage).ListDir)。那么这里到底发生了什么?

自动生成函数

让我们首先检查这个指针接收器函数是关于什么的。通过运行 go tool objdump -s ListDir minio,我们可以获取函数的定义,并发现它显然是 <autogenerated>

TEXT minio/cmd.(*retryStorage).ListDir(SB) <autogenerated>
 <autogenerated>:926  0x1f2e00	GS MOVQ GS:0x8a0, CX
 <autogenerated>:926  0x1f2e09	CMPQ 0x10(CX), SP
 <autogenerated>:926  0x1f2e0d	JBE 0x1f2f42
 <autogenerated>:926  0x1f2e13	SUBQ $0x78, SP
 <autogenerated>:926  0x1f2e17	MOVQ BP, 0x70(SP)
 <autogenerated>:926  0x1f2e1c	LEAQ 0x70(SP), BP
 <autogenerated>:926  0x1f2e21	MOVQ 0x20(CX), BX
 <autogenerated>:926  0x1f2e25	TESTQ BX, BX
 <autogenerated>:926  0x1f2e28	JE 0x1f2e3a
 <autogenerated>:926  0x1f2e2a	LEAQ 0x80(SP), DI
 <autogenerated>:926  0x1f2e32	CMPQ DI, 0(BX)
 <autogenerated>:926  0x1f2e35	JNE 0x1f2e3a
 <autogenerated>:926  0x1f2e37	MOVQ SP, 0(BX)
 <autogenerated>:926  0x1f2e3a	MOVQ 0x80(SP), AX
 <autogenerated>:926  0x1f2e42	TESTQ AX, AX
 <autogenerated>:926  0x1f2e45	JE 0x1f2efa
 <autogenerated>:926  0x1f2e4b	MOVQ 0x80(SP), AX
 <autogenerated>:926  0x1f2e53	MOVQ 0(AX), CX
 <autogenerated>:926  0x1f2e56	MOVQ CX, 0(SP)
 <autogenerated>:926  0x1f2e5a	LEAQ 0x8(AX), SI
 <autogenerated>:926  0x1f2e5e	LEAQ 0x8(SP), DI
 <autogenerated>:926  0x1f2e63	MOVQ BP, -0x10(SP)
 <autogenerated>:926  0x1f2e68	LEAQ -0x10(SP), BP
 <autogenerated>:926  0x1f2e6d	CALL 0x5d5a4
 <autogenerated>:926  0x1f2e72	MOVQ 0(BP), BP
 <autogenerated>:926  0x1f2e76	MOVQ 0x88(SP), AX
 <autogenerated>:926  0x1f2e7e	MOVQ AX, 0x28(SP)
 <autogenerated>:926  0x1f2e83	MOVQ 0x90(SP), AX
 <autogenerated>:926  0x1f2e8b	MOVQ AX, 0x30(SP)
 <autogenerated>:926  0x1f2e90	MOVQ 0x98(SP), AX
 <autogenerated>:926  0x1f2e98	MOVQ AX, 0x38(SP)
 <autogenerated>:926  0x1f2e9d	MOVQ 0xa0(SP), AX
 <autogenerated>:926  0x1f2ea5	MOVQ AX, 0x40(SP)
 <autogenerated>:926  0x1f2eaa	CALL cmd.retryStorage.ListDir(SB)
 <autogenerated>:926  0x1f2eaf	MOVQ 0x48(SP), AX
 <autogenerated>:926  0x1f2eb4	MOVQ 0x50(SP), CX
 <autogenerated>:926  0x1f2eb9	MOVQ 0x58(SP), DX
 <autogenerated>:926  0x1f2ebe	MOVQ 0x60(SP), BX
 <autogenerated>:926  0x1f2ec3	MOVQ 0x68(SP), SI
 <autogenerated>:926  0x1f2ec8	MOVQ AX, 0xa8(SP)
 <autogenerated>:926  0x1f2ed0	MOVQ CX, 0xb0(SP)
 <autogenerated>:926  0x1f2ed8	MOVQ DX, 0xb8(SP)
 <autogenerated>:926  0x1f2ee0	MOVQ BX, 0xc0(SP)
 <autogenerated>:926  0x1f2ee8	MOVQ SI, 0xc8(SP)
 <autogenerated>:926  0x1f2ef0	MOVQ 0x70(SP), BP
 <autogenerated>:926  0x1f2ef5	ADDQ $0x78, SP
 <autogenerated>:926  0x1f2ef9	RET
 <autogenerated>:926  0x1f2efa	LEAQ 0x96c2bb(IP), AX
 <autogenerated>:926  0x1f2f01	MOVQ AX, 0(SP)
 <autogenerated>:926  0x1f2f05	MOVQ $0x3, 0x8(SP)
 <autogenerated>:926  0x1f2f0e	LEAQ 0x976870(IP), AX
 <autogenerated>:926  0x1f2f15	MOVQ AX, 0x10(SP)
 <autogenerated>:926  0x1f2f1a	MOVQ $0xc, 0x18(SP)
 <autogenerated>:926  0x1f2f23	LEAQ 0x9707a2(IP), AX
 <autogenerated>:926  0x1f2f2a	MOVQ AX, 0x20(SP)
 <autogenerated>:926  0x1f2f2f	MOVQ $0x7, 0x28(SP)
 <autogenerated>:926  0x1f2f38	CALL runtime.panicwrap(SB)
 <autogenerated>:926  0x1f2f3d	JMP 0x1f2e4b
 <autogenerated>:926  0x1f2f42	CALL runtime.morestack_noctxt(SB)
 <autogenerated>:926  0x1f2f47	JMP cmd.(*retryStorage).ListDir(SB)

所以我们可以看到,在偏移量 0x1f2eaa 的中间位置,它调用了 retryStorage.ListDir(值接收器),这是我们在 Golang 代码 minio/cmd/retry-storage.go:183 中定义的。在此之前,它在栈上设置了所有参数,包括解引用 *retryStorage 并在栈上创建它的副本(因为 retryStorage.ListDir 需要)。

CALL 之后,返回参数被加载(偏移量 0x1f2eaf0x1f2ec3),并被复制到相应的插槽中(偏移量 0x1f2ec80x1f2ee8),用于 (*retryStorage).ListDir 的返回参数,之后自动生成的函数返回。

所以,(*retryStorage).ListDir 本质上所做的就是包装对 retryStorage.ListDir 的调用,并确保指针接收器指向的对象不会被修改。

为什么 Golang 会这样做?

既然我们知道了 (*retryStorage).ListDir 的作用,下一个问题是,为什么 Golang 会这样做?这与 StorageAPI 是一个接口类型有关,因此 cleanupDir 中的 storage 参数是一个接口值,实际上是一个双指针结构:接口值表示为一对两字,给出指向存储在接口中的类型的信息的指针和指向关联数据的指针。

所以当我们调用 storage.ListDir 时,我们会遇到这样一个情况:我们只能访问指向 retryStorage 的指针,而只能使用 ListDir 的值接收器方法。这时,Golang 编译器就足够友好地为我们(自动)生成它,尽管这确实会带来相当大的执行成本,因为我们需要解引用我们的对象并复制返回参数等等。感谢 @thatcks 提供了对此问题的说明。

如果你不想这样做怎么办?

首先,你并不一定需要 “修复” 任何东西,因为没有任何内在错误,你的代码将正常运行。

但是,如果你确实想修复它,这非常简单:只需将 func (f retryStorage) ListDir(...) 更改为 func (f *retryStorage) ListDir(...),位于 minio/cmd/retry-storage.go:183,你就可以像以下内容一样设置好了。

TEXT github.com/minio/minio/cmd.(*retryStorage).ListDir(SB)
 retry-storage.go:199	0x15edc0	GS MOVQ GS:0x8a0, CX
 retry-storage.go:199	0x15edc9	CMPQ 0x10(CX), SP
 retry-storage.go:199	0x15edcd	JBE 0x15efe5
 <rest omitted>

显然,你将需要确保你的函数实现不会意外地更改 f 的值,因为你会开始注意到这一点(这很可能有点奇怪,因为通常更改 f 内容的意图应该是由调用者来注意的,这在值接收器函数中不会发生)。

而且,作为免费赠送,你将减少可执行文件的大小(对于这个单个函数,仅仅在源代码文件中添加一个 * 就能获得如此不错的结果……)。

结论

希望这篇博文能让你对 Golang 函数的内部工作原理有所了解,并阐明这些自动生成函数是什么以及你可以对它们做些什么。

在本系列的后续文章中,我们还将详细介绍指针接收器函数与值接收器函数的性能对比。