关于Go的四种字符串拼接及性能比较'
先上代码
// fmt.Sprintf
func BenchmarkStringSprintf(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var str string
for j := 0; j < numbers; j++ {
str = fmt.Sprintf("%s%d", str, j)
}
}
b.StopTimer()
}
// add
func BenchmarkStringAdd(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var str string
for j := 0; j < numbers; j++ {
str = str + string(j)
}
}
b.StopTimer()
}
// bytes.Buffer
func BenchmarkStringBuffer(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
for j := 0; j < numbers; j++ {
buffer.WriteString(strconv.Itoa(j))
}
_ = buffer.String()
}
b.StopTimer()
}
// strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var builder strings.Builder
for j := 0; j < numbers; j++ {
builder.WriteString(strconv.Itoa(j))
}
_ = builder.String()
}
b.StopTimer()
}
运行结果
lijun:benchmark/ $ go test -bench=. [10:01:20]
goos: darwin
goarch: amd64
pkg: class12/benchmark
BenchmarkStringSprintf-4 30 47358694 ns/op
BenchmarkStringAdd-4 50 27664814 ns/op
BenchmarkStringBuffer-4 10000 184422 ns/op
BenchmarkStringBuilder-4 10000 157039 ns/op
PASS
ok class12/benchmark 6.350s
lijun:benchmark/ $ [10:01:58]
如果还不知道 Go 的 benchmark,可以先去了解一下,个人认为还是非常不错的性能测试的工具。
得出结论
四种拼接字符串的方式,性能比较结果
strings.Builder > bytes.Buffer > string add > fmt.Sprintf
为什么?
这里我们还是直接看源码吧
先看 Sprintf
// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...interface{}) string {
p := newPrinter()
p.doPrintf(format, a)
s := string(p.buf)
p.free()
return s
}
这是 fmt.Sprintf 的源码,我们可以看到内部会通过 newPrinter 创建一个新对象 p,点进去看一下 newPrinter 这个函数
// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.fmt.init(&p.buf)
return p
}
它会从系统的临时对象池中那 pp 这个对象,关于临时对象池(sync.Pool),下次有机会再探讨。这里可以知道, Sprintf 会从临时对象池中获取一个 *pp 的指针,然后再做一些格式化的操作,doPrintf 代码就不贴了,格式化后的底层字节会放到 []byte 这个切片里面,最后再 string 转换成字符串返回,并且释放掉 p 对象。
整个过程:创建对象 - 格式化操作 - string化 - 释放对象
接下来看 string
string 是在 Go 里面是一个不可变类型,所以下面的代码
str = str + str2
每次都会创建一个新的 string 类型的值,然后重新赋值给 str 这个变量,相比于上面的 Sprintf 主要少了格式化这个过程,所以在性能上肯定要优于 Sprintf
bytes.Buffer
我们看一下 builder 的 String() 函数源码
// String returns the contents of the unread portion of the buffer
// as a string. If the Buffer is a nil pointer, it returns "<nil>".
//
// To build strings more efficiently, see the strings.Builder type.
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
字符串的底层结构是一个 []byte 的字节序列,而 Buffer 是直接获取未读取的 []byte序列,在转成 string 返回,少了重复创建对象这个步骤。
b.buf 是 []byte 切片
b.off 是已读取的字节位置
strings.Builder
// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
strings.Builder 直接通过指针来操作了,在效率上更进一步。
总结
通过源码来分析,还是比较清晰明了的,但是限于我自身的水平,对于源码的解读并不都是特别深入,这里也是给大家做出一个参考。关于最后的通过转换成指针来返回字符串的操作,我也就是知道转成指针效率会高,但是关于为什么,也都是模棱两可(是因为直接通过操作内存地址吗)。总之关于基础性、底层的东西还是要多多学习。