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.ListDir
,storage
被传递到 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
之后,返回参数被加载(偏移量 0x1f2eaf
到 0x1f2ec3
),并被复制到相应的插槽中(偏移量 0x1f2ec8
到 0x1f2ee8
),用于 (*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 函数的内部工作原理有所了解,并阐明这些自动生成函数是什么以及你可以对它们做些什么。
在本系列的后续文章中,我们还将详细介绍指针接收器函数与值接收器函数的性能对比。