本文译自(Golang 内幕第 2 部分: 命名返回值的好处)版权@归原文所有
你可能知道 Golang 提供了命名返回值的能力. 到目前为止在 minio 中我们还没有使用这个功能, 但是这将会改变, 因为我们将在这个博客文章中解释一些隐藏的好处.
如果你像我们一样, 你可能会有相当数量的代码, 如下所示, 对于每一个 return 语句你都实例化一个新的对象, 以便返回一个’默认’值:
type objectInfo struct {
arg1 int64
arg2 uint64
arg3 string
arg4 []int
}
func NoNamedReturnParams(i int) (objectInfo) {
if i == 1 {
// Do one thing
return objectInfo{}
}
if i == 2 {
// Do another thing
return objectInfo{}
}
if i == 3 {
// Do one more thing still
return objectInfo{}
}
// Normal return
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 {
// Do one thing
return
}
if i == 2 {
// Do another thing
return
}
if i == 3 {
// Do one more thing still
return
}
// Normal 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, 同时还可以获得相同的好处, 如下所示:
if i == 1 {
return oi
}
minio 服务器中真实世界的例子
我们拿 minio server 的例子更进一步:
// parse credentialHeader string into its structured form.
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 /*!isAccessKeyValid(credElements[0])*/ {
return credentialHeader{}
}
// Save access key id.
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 个字节, 这对于源代码这样一个最小的改变还不错. 取决于你从哪里来,你也可能更喜欢源代码的更干净的外观:
// parse credentialHeader string into its structured form.
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 /*!isAccessKeyValid(credElements[0])*/ {
return
}
// Save access key id.
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 语句的结果调用的函数中访问指定的返回值, 并相应地进行操作.
关于这个系列
如果你错过了本系列的第一部分, 这里是一个链接:
- 关于 autogenerated 函数
结论
所以我们将逐渐采用命名的返回值, 无论是新代码还是现有代码.
事实上, 我们也在研究是否可以开发一些小工具来帮助或自动化这个过程. 按照 gofmt 的思路思考, 然后自动修改源代码以进行上面所述的更改. 特别是在返回值还没有被命名的情况下(因此实用程序必须给它一个名字), 这个返回变量在现有的源代码中以任何方式改变都是不可能的, ch (在上面列表的情况下)不会导致程序的任何功能变化.
所以请继续关注.
我们希望这篇文章能对你有所帮助, 并提供一些關於 Go 如何在內部運行以及如何改進 Golang 代碼的新見解.
更新
已经有一个 Golang issue 来优化编译器为上述情况生成相同的代码, 这将是一件好事.