ℹ️ 本文基于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