string
是一种原始类型,这意味着它是只读的,对其进行任何操作都会创建一个新字符串。因此,如果我想要多次连接字符串而不知道结果字符串的长度,最好的方法是什么?幼稚的方式是:var s string
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
但那似乎不是很高效。
string
是一种原始类型,这意味着它是只读的,对其进行任何操作都会创建一个新字符串。因此,如果我想要多次连接字符串而不知道结果字符串的长度,最好的方法是什么?幼稚的方式是:var s string
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
但那似乎不是很高效。
从Go 1.10开始,有一个strings.Builder
类型,请参考此答案以获取更多细节。
使用bytes
包。它有一个Buffer
类型,实现了io.Writer
接口。
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)。
var buffer bytes.Buffer
替换 buffer := bytes.NewBufferString("")
。您还不需要任何分号 : )。 - crazy2bestrings.Builder
的工具,在这里。
使用 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())
}
strings.Builder
的方法是按照现有接口进行实现的,以便您可以轻松地在代码中切换到新的 Builder
类型。
方法签名 | 接口 | 描述 |
---|---|---|
Grow(int) |
bytes.Buffer |
按指定的数量增加缓冲区的容量。有关更多信息,请参见bytes.Buffer#Grow。 |
Len() int |
bytes.Buffer |
返回缓冲区中的字节数。有关更多信息,请参见bytes.Buffer#Len。 |
Reset() |
bytes.Buffer |
将缓冲区重置为空。有关更多信息,请参见bytes.Buffer#Reset。 |
String() string |
fmt.Stringer |
将缓冲区的内容作为字符串返回。有关更多信息,请参见fmt.Stringer。 |
Write([]byte) (int, error) |
io.Writer |
将给定的字节写入缓冲区。有关更多信息,请参见io.Writer。 |
WriteByte(byte) error |
io.ByteWriter |
将给定的字节写入缓冲区。有关更多信息,请参见io.ByteWriter。 |
WriteRune(rune) (int, error) |
bufio.Writer 或 bytes.Buffer |
将给定的符文写入缓冲区。有关更多信息,请参见bufio.Writer#WriteRune或bytes.Buffer#WriteRune。 |
WriteString(string) (int, error) |
io.stringWriter |
将给定的字符串写入缓冲区。有关更多信息,请参见io.stringWriter。 |
copyCheck
机制,可以防止意外复制。在bytes.Buffer
中,可以像这样访问底层字节:(*Buffer).Bytes()
。strings.Builder
解决了这个问题。但有时,这不是问题,反而是所期望的。例如:当字节被传递给io.Reader
等时,需要查看其行为。bytes.Buffer.Reset()
重新定位并重用底层缓冲区,而strings.Builder.Reset()
不会,它会分离缓冲区。strings.Builder
值,因为它会缓存底层数据。strings.Builder
值,请使用指向它的指针。查看其源代码以获取更多详细信息,在这里。
strings.Builder.Reset()
将底层切片设置为nil
(不重用内存)。而bytes.Buffer.Reset()
将[]bytes
设置为空长度,保持底层数组分配。当我在sync.Pool
中重用strings.Builder
时,这个问题困扰了我,看起来完全没有用处。 - Tim如果你知道预先分配字符串的总长度,那么最高效的字符串拼接方式可能是使用内置函数copy
。如果你事先不知道总长度,请勿使用copy
,请阅读其他答案。
在我的测试中,这种方法比使用bytes.Buffer
要快大约3倍,并且比使用操作符+
要快得多(约为12000倍)。此外,它使用的内存更少。
我创建了一个测试用例来证明这一点,以下是结果:
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)
}
}
buffer.Write
(字节)比buffer.WriteString
快30%。 [如果您可以将数据作为“[]byte”获取,则非常有用] - Dani-Brb.N
值调用,因此您不能比较执行相同任务所需的时间(例如,一个函数可能会附加1,000
个字符串,另一个函数可能会附加10,000
个,这可能会对每个附加的平均时间在BenchmarkConcat()
中造成很大的差异)。您应该在每种情况下使用相同的附加计数(肯定不是b.N
),并在for
范围内的体内进行所有串联(也就是说,嵌套2个for
循环)。 - iczaimport (
"fmt";
"strings";
)
func main() {
s := []string{"this", "is", "a", "joined", "string\n"};
fmt.Printf(strings.Join(s, " "));
}
$ ./test.bin
this is a joined string
我刚刚在我的代码中对上面发布的最佳答案进行了基准测试(递归树遍历),发现简单的连接运算符比BufferString
更快。
func (r *record) String() string {
buffer := bytes.NewBufferString("");
fmt.Fprint(buffer,"(",r.name,"[")
for i := 0; i < len(r.subs); i++ {
fmt.Fprint(buffer,"\t",r.subs[i])
}
fmt.Fprint(buffer,"]",r.size,")\n")
return buffer.String()
}
这个操作只需要0.81秒,而以下代码:
func (r *record) String() string {
s := "(\"" + r.name + "\" ["
for i := 0; i < len(r.subs); i++ {
s += r.subs[i].String()
}
s += "] " + strconv.FormatInt(r.size,10) + ")\n"
return s
}
仅用了0.61秒。这可能是由于创建新的BufferString
的开销。
更新:我还对join
函数进行了基准测试,其运行时间为0.54秒。
func (r *record) String() string {
var parts []string
parts = append(parts, "(\"", r.name, "\" [" )
for i := 0; i < len(r.subs); i++ {
parts = append(parts, r.subs[i].String())
}
parts = append(parts, strconv.FormatInt(r.size,10), ")\n")
return strings.Join(parts,"")
}
buffer.WriteString("\t");
和 buffer.WriteString(subs[i]);
可能会导致速度变慢。 - Robert Jack Willpackage main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
根据我的基准测试,使用此方法比复制方案慢20%(每个附加操作需要8.1纳秒而不是6.72纳秒),但仍然比使用bytes.Buffer快55%。
func Append(slice, data[]byte) []byte {
l := len(slice);
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2);
// Copy data (could use bytes.Copy()).
for i, c := range slice {
newSlice[i] = c
}
slice = newSlice;
}
slice = slice[0:l+len(data)];
for i, c := range data {
slice[l+i] = c
}
return slice;
}
然后当操作完成时,使用string()
将大块字节转换回字符串。
append(slice, byte...)
来替换你的函数,看起来是这样的。 - Aktau从Go 1.10开始,有一个 strings.Builder
类型,请参考这个答案以获取更多详细信息。
@cd1 和其他答案的基准测试代码是错误的。b.N
不应该在基准测试函数中设置。它由 go test 工具动态设置,以确定测试的执行时间是否稳定。
基准测试函数应该运行相同的测试 b.N
次,并且循环内部的测试应该对于每次迭代都是相同的。所以我添加了一个内部循环来修复它。我还为其他一些解决方案添加了基准测试:
package main
import (
"bytes"
"strings"
"testing"
)
const (
sss = "xfoasneobfasieongasbg"
cnt = 10000
)
var (
bbb = []byte(sss)
expected = strings.Repeat(sss, cnt)
)
func BenchmarkCopyPreAllocate(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
bs := make([]byte, cnt*len(sss))
bl := 0
for i := 0; i < cnt; i++ {
bl += copy(bs[bl:], sss)
}
result = string(bs)
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkAppendPreAllocate(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
data := make([]byte, 0, cnt*len(sss))
for i := 0; i < cnt; i++ {
data = append(data, sss...)
}
result = string(data)
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkBufferPreAllocate(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
buf := bytes.NewBuffer(make([]byte, 0, cnt*len(sss)))
for i := 0; i < cnt; i++ {
buf.WriteString(sss)
}
result = buf.String()
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkCopy(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
data := make([]byte, 0, 64) // same size as bootstrap array of bytes.Buffer
for i := 0; i < cnt; i++ {
off := len(data)
if off+len(sss) > cap(data) {
temp := make([]byte, 2*cap(data)+len(sss))
copy(temp, data)
data = temp
}
data = data[0 : off+len(sss)]
copy(data[off:], sss)
}
result = string(data)
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkAppend(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
data := make([]byte, 0, 64)
for i := 0; i < cnt; i++ {
data = append(data, sss...)
}
result = string(data)
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkBufferWrite(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
var buf bytes.Buffer
for i := 0; i < cnt; i++ {
buf.Write(bbb)
}
result = buf.String()
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkBufferWriteString(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
var buf bytes.Buffer
for i := 0; i < cnt; i++ {
buf.WriteString(sss)
}
result = buf.String()
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
func BenchmarkConcat(b *testing.B) {
var result string
for n := 0; n < b.N; n++ {
var str string
for i := 0; i < cnt; i++ {
str += sss
}
result = str
}
b.StopTimer()
if result != expected {
b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
}
}
环境是OS X 10.11.6,2.2 GHz英特尔Core i7
测试结果:
BenchmarkCopyPreAllocate-8 20000 84208 ns/op 425984 B/op 2 allocs/op
BenchmarkAppendPreAllocate-8 10000 102859 ns/op 425984 B/op 2 allocs/op
BenchmarkBufferPreAllocate-8 10000 166407 ns/op 426096 B/op 3 allocs/op
BenchmarkCopy-8 10000 160923 ns/op 933152 B/op 13 allocs/op
BenchmarkAppend-8 10000 175508 ns/op 1332096 B/op 24 allocs/op
BenchmarkBufferWrite-8 10000 239886 ns/op 933266 B/op 14 allocs/op
BenchmarkBufferWriteString-8 10000 236432 ns/op 933266 B/op 14 allocs/op
BenchmarkConcat-8 10 105603419 ns/op 1086685168 B/op 10000 allocs/op
结论:
CopyPreAllocate
是最快的方式;AppendPreAllocate
很接近第一,但它更容易编写代码。Concat
的性能非常糟糕,无论是速度还是内存使用。不要使用它。Buffer#Write
和 Buffer#WriteString
在速度上基本相同,与评论中@Dani-Br所说的相反。考虑到在Go中,string
实际上是[]byte
,这是有道理的。Copy
相同的解决方案,带有额外的记录和其他内容。Copy
和 Append
使用 64 的引导大小,与 bytes.Buffer 相同。Append
使用更多的内存和分配,我认为这与其使用的增长算法有关。它的内存增长速度没有 bytes.Buffer 快。建议:
Append
或AppendPreAllocate
。它足够快且易于使用。bytes.Buffer
。那是它的设计初衷。我的原始建议是
s12 := fmt.Sprint(s1,s2)
然而,使用bytes.Buffer - WriteString()的答案是最有效的。
我的初始建议使用了反射和类型开关。请参见(p *pp) doPrint
和(p *pp) printArg
,基本类型没有通用的Stringer()接口,这是我天真的想法。
至少,Sprint()在内部使用了一个bytes.Buffer。因此
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
在内存分配方面是可接受的。
=> Sprint()拼接可以用于快速调试输出。
=> 否则使用bytes.Buffer ... WriteString
append()
进入语言之前编写的,这是一个很好的解决方案。它将像copy()
一样快速执行,但即使这意味着需要分配一个新的后备数组来增加切片的容量,它也会先增加切片。如果你想使用其附加的便利方法或你正在使用的包期望它,bytes.Buffer
仍然是有意义的。 - thomasrutter1 + 2 + 3 + 4 + ...
。它是n*(n+1)/2
,即基于n的三角形面积。当您在循环中附加不可变字符串时,您分配大小为1,然后大小为2,然后大小为3等等。这种二次资源消耗表现方式不仅限于此。 - Rob