本文译自(
Golang 内幕第 2 部分: 命名返回值的好处)版权@归原文所有
你可能知道 Golang 提供了命名返回值的能力. 到目前为止在 minio 中我们还没有使用这个功能, 但是这将会改变, 因为我们将在这个博客文章中解释一些隐藏的好处.
如果你像我们一样, 你可能会有相当数量的代码, 如下所示, 对于每一个 return 语句你都实例化一个新的对象, 以便返回一个’默认’值:
type objectInfo struct { arg1 int64 arg2 uint64 arg3 string arg4 []int } func NoNamedReturnParams(i int) (objectInfo) {
if i == 1 { return objectInfo{} }
if i == 2 { return objectInfo{} }
if i == 3 { return objectInfo{} }
return objectInfo{} }
|
如果你看一下 Golang 编译器生成的实际代码, 你将会得到如下的结果:
"".NoNamedReturnParams t=1 size=243 args=0x40 locals=0x0 0x0000 TEXT "".NoNamedReturnParams(SB), $0-64 0x0000 MOVQ $0, "".~r1+16(FP) 0x0009 LEAQ "".~r1+24(FP), DI 0x000e XORPS X0, X0 0x0011 ADDQ $-16, DI 0x0015 DUFFZERO $288 0x0028 MOVQ "".i+8(FP), AX 0x002d CMPQ AX, $1 0x0031 JEQ $0, 199 0x0037 CMPQ AX, $2 0x003b JEQ $0, 155 0x003d CMPQ AX, $3 0x0041 JNE 111 0x0043 MOVQ "".statictmp_2(SB), AX 0x004a MOVQ AX, "".~r1+16(FP) 0x004f LEAQ "".~r1+24(FP), DI 0x0054 LEAQ "".statictmp_2+8(SB), SI 0x005b DUFFCOPY $854 0x006e RET 0x006f MOVQ "".statictmp_3(SB), AX 0x0076 MOVQ AX, "".~r1+16(FP) 0x007b LEAQ "".~r1+24(FP), DI 0x0080 LEAQ "".statictmp_3+8(SB), SI 0x0087 DUFFCOPY $854 0x009a RET 0x009b MOVQ "".statictmp_1(SB), AX 0x00a2 MOVQ AX, "".~r1+16(FP) 0x00a7 LEAQ "".~r1+24(FP), DI 0x00ac LEAQ "".statictmp_1+8(SB), SI 0x00b3 DUFFCOPY $854 0x00c6 RET 0x00c7 MOVQ "".statictmp_0(SB), AX 0x00ce MOVQ AX, "".~r1+16(FP) 0x00d3 LEAQ "".~r1+24(FP), DI 0x00d8 LEAQ "".statictmp_0+8(SB), SI 0x00df DUFFCOPY $854 0x00f2 RET
|
一切都很好, 但这看起来是否有点重复? 你是对的. 实质上, 对于每个 return 语句, 要返回的对象或多或少被分配/初始化(或者通过 DUFFCOPY 宏更精确地复制).
毕竟这是我们通过在每种情况下都返回 objectInfo {} 的结果.
命名返回值
现在看看如果我们做一个非常简单的改变会发生什么, 本质上只是给返回值一个名字 (oi) 和使用 Golang 的’裸体’返回特性(为返回语句放弃参数, 虽然这不是严格要求, 稍后更多):
func NamedReturnParams(i int) (oi objectInfo) {
if i == 1 { return }
if i == 2 { return }
if i == 3 { return }
return }
|
再看看编译器生成的代码, 我们得到以下结果:
"".NamedReturnParams t=1 size=67 args=0x40 locals=0x0 0x0000 TEXT "".NamedReturnParams(SB), $0-64 0x0000 MOVQ $0, "".oi+16(FP) 0x0009 LEAQ "".oi+24(FP), DI 0x000e XORPS X0, X0 0x0011 ADDQ $-16, DI 0x0015 DUFFZERO $288 0x0028 MOVQ "".i+8(FP), AX 0x002d CMPQ AX, $1 0x0031 JEQ $0, 66 0x0033 CMPQ AX, $2 0x0037 JEQ $0, 65 0x0039 CMPQ AX, $3 0x003d JNE 64 0x003f RET 0x0040 RET 0x0041 RET 0x0042 RET
|
这是一个非常大的差异, 所有四个对象初始化和 DUFFCOPY 这些东西消失(甚至对于这个微不足道的情况)了. 它将函数的大小从 243 减小到 67 字节. 另外作为一个额外的好处, 你将省去一些 CPU 周期退出, 因为不需要做任何事情来设置返回值.
请注意, 如果您不喜欢或偏好 Golang 提供的裸返回, 则可以使用 return oi, 同时还可以获得相同的好处, 如下所示:
minio 服务器中真实世界的例子
我们拿 minio server 的例子更进一步:
func parseCredentialHeader(credElement string) credentialHeader { creds := strings.Split(strings.TrimSpace(credElement), "=") if len(creds) != 2 { return credentialHeader{} } if creds[0] != "Credential" { return credentialHeader{} } credElements := strings.Split(strings.TrimSpace(creds[1]), "/") if len(credElements) != 5 { return credentialHeader{} } if false { return credentialHeader{} } cred := credentialHeader{ accessKey: credElements[0], } var e error cred.scope.date, e = time.Parse(yyyymmdd, credElements[1]) if e != nil { return credentialHeader{} } cred.scope.region = credElements[2] if credElements[3] != "s3" { return credentialHeader{} } cred.scope.service = credElements[3] if credElements[4] != "aws4_request" { return credentialHeader{} } cred.scope.request = credElements[4] return cred }
|
深入汇编我们得到以下的函数头:
"".parseCredentialHeader t=1 size=1157 args=0x68 locals=0xb8
|
如果我们修改代码来使用一个命名返回参数(下面的第二个源代码块), 请检查函数的大小:
"".parseCredentialHeader t=1 size=863 args=0x68 locals=0xb8
|
它从总共 1150 个字节中删除了 300 个字节, 这对于源代码这样一个最小的改变还不错. 取决于你从哪里来,你也可能更喜欢源代码的更干净的外观:
func parseCredentialHeader(credElement string) (ch credentialHeader) { creds := strings.Split(strings.TrimSpace(credElement), "=") if len(creds) != 2 { return } if creds[0] != "Credential" { return } credElements := strings.Split(strings.TrimSpace(creds[1]), "/") if len(credElements) != 5 { return } if false { return } cred := credentialHeader{ accessKey: credElements[0], } var e error cred.scope.date, e = time.Parse(yyyymmdd, credElements[1]) if e != nil { return } cred.scope.region = credElements[2] if credElements[3] != "s3" { return } cred.scope.service = credElements[3] if credElements[4] != "aws4_request" { return } cred.scope.request = credElements[4] return cred }
|
请注意, 实际上 ch 变量是一个正常的局部变量, 就像在函数中定义的任何其他局部变量一样. 因此, 您可以将其值从默认的’零’状态更改(当然, 修改后的版本将在退出时返回).
命名返回值的其他用法
正如几位人士指出的那样, 指定返回值的另一个好处是可以在闭包中使用(即 defer 语句). 因此, 可以在作为 defer 语句的结果调用的函数中访问指定的返回值, 并相应地进行操作.
关于这个系列
如果你错过了本系列的第一部分, 这里是一个链接:
结论
所以我们将逐渐采用命名的返回值, 无论是新代码还是现有代码.
事实上, 我们也在研究是否可以开发一些小工具来帮助或自动化这个过程. 按照 gofmt 的思路思考, 然后自动修改源代码以进行上面所述的更改. 特别是在返回值还没有被命名的情况下(因此实用程序必须给它一个名字), 这个返回变量在现有的源代码中以任何方式改变都是不可能的, ch (在上面列表的情况下)不会导致程序的任何功能变化.
所以请继续关注.
我们希望这篇文章能对你有所帮助, 并提供一些关于 Go 如何在内部运行以及如何改进 Golang 代码的新见解.
更新
已经有一个 Golang issue 来优化编译器为上述情况生成相同的代码, 这将是一件好事.