在 Go 中, string
是原始类型,这意味着它是只读的,对它的每次操作都会创建一个新的字符串。
因此,如果我想在不知道结果字符串长度的情况下多次连接字符串,最好的方法是什么?
天真的方法是:
var s string
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
但这似乎不是很有效。
从 Go 1.10 开始,有一个strings.Builder
类型, 请查看此答案以获取更多详细信息。
使用bytes
包。它具有实现io.Writer
Buffer
类型。
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
for i := 0; i < 1000; i++ {
buffer.WriteString("a")
}
fmt.Println(buffer.String())
}
这是在 O(n)时间内完成的。
在 Go 1.10 + 中,这里strings.Builder
。
生成器用于使用 Write 方法有效地构建字符串。它最大程度地减少了内存复制。零值可以使用了。
bytes.Buffer
几乎相同。
package main
import (
"strings"
"fmt"
)
func main() {
// ZERO-VALUE:
//
// It's ready to use from the get-go.
// You don't need to initialize it.
var sb strings.Builder
for i := 0; i < 1000; i++ {
sb.WriteString("a")
}
fmt.Println(sb.String())
}
正在考虑现有接口的情况下实现 StringBuilder 的方法。这样您就可以在代码中轻松切换到新的 Builder 类型。
它只能增长或重置。
它具有内置的 copyCheck 机制,可防止意外复制它:
func (b *Builder) copyCheck() { ... }
在bytes.Buffer
,可以像这样访问基础字节: (*Buffer).Bytes()
。
strings.Builder
可以防止出现此问题。io.Reader
等时的偷窥行为。 bytes.Buffer.Reset()
倒带并重用基础缓冲区,而strings.Builder.Reset()
则不,它分离缓冲区。
在此处查看其源代码以获取更多详细信息。
如果知道要预分配的字符串的总长度,则连接字符串的最有效方法可能是使用内置函数copy
。如果您不知道事前的总长度,请不要使用copy
,而应阅读其他答案。
在我的测试中,这种方法比使用bytes.Buffer
+
快得多(〜12,000x)。同样,它使用更少的内存。
我创建了一个测试案例来证明这一点,结果如下:
BenchmarkConcat 1000000 64497 ns/op 502018 B/op 0 allocs/op
BenchmarkBuffer 100000000 15.5 ns/op 2 B/op 0 allocs/op
BenchmarkCopy 500000000 5.39 ns/op 0 B/op 0 allocs/op
以下是测试代码:
package main
import (
"bytes"
"strings"
"testing"
)
func BenchmarkConcat(b *testing.B) {
var str string
for n := 0; n < b.N; n++ {
str += "x"
}
b.StopTimer()
if s := strings.Repeat("x", b.N); str != s {
b.Errorf("unexpected result; got=%s, want=%s", str, s)
}
}
func BenchmarkBuffer(b *testing.B) {
var buffer bytes.Buffer
for n := 0; n < b.N; n++ {
buffer.WriteString("x")
}
b.StopTimer()
if s := strings.Repeat("x", b.N); buffer.String() != s {
b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s)
}
}
func BenchmarkCopy(b *testing.B) {
bs := make([]byte, b.N)
bl := 0
b.ResetTimer()
for n := 0; n < b.N; n++ {
bl += copy(bs[bl:], "x")
}
b.StopTimer()
if s := strings.Repeat("x", b.N); string(bs) != s {
b.Errorf("unexpected result; got=%s, want=%s", string(bs), s)
}
}
// Go 1.10
func BenchmarkStringBuilder(b *testing.B) {
var strBuilder strings.Builder
b.ResetTimer()
for n := 0; n < b.N; n++ {
strBuilder.WriteString("x")
}
b.StopTimer()
if s := strings.Repeat("x", b.N); strBuilder.String() != s {
b.Errorf("unexpected result; got=%s, want=%s", strBuilder.String(), s)
}
}