Go:Stringer命令,通过代码生成提高效率

ℹ️ 本文基于Go 1.13。

stringer命令旨在自动创建满足fmt.Stringer的方法。 它为指定类型生成String()并将其描述为字符串。常可用于​定义错误码时同时生成错误信息等场景。​

案例

该命令的文档提供了一个该命令的示例。 如下:

这里是输出:

1

用常量的值生成日志可能会有些混乱。

让我们使用stringer -type=Pill命令生成String()方法:

//stringer并不是 Go 自带的工具,需要手动安装。可以执行下面的命令安装:
​go get golang.org/x/tools/cmd/stringer

生成了一个新的函数String()。 这是运行先前代码时的新输出:

Aspirin

现在,该类型将自身描述为字符串,而不是其内部值。

stringer也可以与生成命令完美配合,使其功能更加强大。 只需在代码中添加以下指令即可:

然后,运行go generate命令将自动为您的所有类型生成新函数。

效率

stringer生成一个字符串,该字符串包含作为字符串的值的列表,以及一个包含每个字符串的索引的数组。 在我们的示例中,读取Aspirin将包括从索引7到13读取字符串:

但是它有多快和高效? 让我们与其他两种解决方案进行比较:

  • 使用硬编码值生成String()函数:

这是一个包含二十个值的列表的基准:

name                  time/op
Stringer-4            4.16ns ± 2%
StringerWithSwitch-4  3.81ns ± 1%

这是具有一百个值的基准:

name                  time/op
Stringer-4            4.96ns ± 0%
StringerWithSwitch-4  4.99ns ± 1%

您拥有的常数越多,效率越高。 这实际上是有道理的。 从内存中加载值比执行一些跳转指令(表示if条件的汇编指令)要耗费更多的时间。但是,switch越大,跳转指令的数量就越大。 从某个角度来看,从内存中加载将变得更加有效。

  • String()函数生成一个map:

这是一个包含二十个值的列表的基准:

name                  time/op
Stringer-4             4.16ns ± 2%
StringerWithMap-4     28.60ns ± 2%

使用map的速度要慢得多,因为它必须进行函数调用,并且存储桶中的查找不像访问切片索引那样简单。

自检

在生成的指令中,仅出于验证目的而创建了一些指令。 以下是这些说明:

stringer将每行的常量名称与该值一起写入。 在此示例中,Aspirin的值为2。更新常量名称或其值将生成错误:

  • 更新名称而不重新生成String()函数:
./pill_string.go:12:8: undefined: Aspirin
  • 更新值而不重新生成String()函数:
./pill_string.go:12:7: invalid array index Aspirin - 1 (out of bounds for 1-element array)
./pill_string.go:13:7: invalid array index Ibuprofen - 2 (index must be non-negative

但是,如果我们添加一个新的常量(这里的下一个键是数字3)并且不更新生成的文件,则stringer具有默认值:

Pill(3)

添加此自检不会产生任何影响,因为在编译时将其删除。 可以通过查看程序生成的asm代码来确认:

➜  go tool compile -S main.go pill_string.go | grep "\"\".Pill\.[^\s]* STEXT"
"".Pill.String STEXT size=275 args=0x18 locals=0x50

只有String()函数以二进制形式导出。 该检查没有性能或二进制大小的开销。

编译整理自  Go: Stringer Command, Efficiency Through Code Generation

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据