Go 语言内部机制第二部分:命名返回值的妙处
您可能知道 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
宏复制)。
毕竟,这就是我们通过在每种情况下使用return objectInfo{}
返回所要求的。
命名返回值
现在看看如果我们进行一个非常简单的更改会发生什么,本质上只是给返回值一个名称(oi
)并使用 Golang 的“裸”返回功能(删除return
语句的参数,尽管这并非严格要求,稍后会详细介绍)。
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 服务器 中进一步检查,我们采用了以下案例。
// 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
语句的结果调用的函数中访问命名返回值并相应地采取行动。
关于本系列
如果您错过了本系列的第一部分,这里有一个链接。
- 关于自动生成 函数
结论
因此,我们将逐渐越来越多地采用命名返回值,包括新代码和现有代码。
事实上,我们还在研究是否可以开发一个小工具来帮助或自动化此过程。想想gofmt
,但随后自动修改源代码以进行上述更改。特别是在返回值尚未命名的情况下(因此实用程序必须为其命名),它必然不能更改现有源代码中此返回值的任何方式,因此使用return ch
(在上面的列表中)将不会导致程序的任何功能性更改。敬请期待。
我们希望这篇文章对您有所帮助,并为您提供一些关于 Go 内部运行机制以及如何改进您的 Golang 代码的新见解。
更新
一个问题 已经提交给 Golang,要求优化编译器以针对上述情况生成相同的代码,这将是一件好事。