如何在Go中高效地连接string?

在Go中,string是一个原始types,它是只读的,对它的每一个操作都会创build一个新的string。

所以,如果我想多次连接string而不知道结果string的长度,那么最好的方法是什么?

天真的方式是:

s := "" for i := 0; i < 1000; i++ { s += getShortStringFromSomewhere() } return s 

但这似乎不是很有效。

最好的方法是使用bytes包。 它有一个实现io.WriterBuffertypes。

 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)时间。

连接string的最有效方法是使用内置函数copy 。 在我的testing中,这个方法比使用bytes.Buffer快了3倍(〜12,000x)比使用operator +更快。 而且,它使用更less的内存。

我创build了一个testing用例来certificate这一点,下面是结果:

 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 

以下是testing代码:

 package main import ( "bytes" "strings" "testing" ) func BenchmarkConcat(b *testing.B) { var str string for n := 0; n < bN; n++ { str += "x" } b.StopTimer() if s := strings.Repeat("x", bN); 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 < bN; n++ { buffer.WriteString("x") } b.StopTimer() if s := strings.Repeat("x", bN); buffer.String() != s { b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s) } } func BenchmarkCopy(b *testing.B) { bs := make([]byte, bN) bl := 0 b.ResetTimer() for n := 0; n < bN; n++ { bl += copy(bs[bl:], "x") } b.StopTimer() if s := strings.Repeat("x", bN); string(bs) != s { b.Errorf("unexpected result; got=%s, want=%s", string(bs), s) } } 

string包中有一个名为Join的库函数: http : //golang.org/pkg/strings/#Join

看看Join的代码显示类似的方法来追加函数Kinopiko写道: https ://golang.org/src/strings/strings.go#L462

用法:

 import ( "fmt"; "strings"; ) func main() { s := []string{"this", "is", "a", "joined", "string\n"}; fmt.Printf(strings.Join(s, " ")); } $ ./test.bin this is a joined string 

我只是在我自己的代码(recursion树行)中testing了上面发布的顶级答案,而简单的concat操作符实际上比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.81s,而下面的代码:

 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.61s。 这可能是由于创build新的BufferStrings的开销。

更新:我也基准连接function,它运行在0.54s

 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,"") } 

这是最快的解决scheme,不需要您首先知道或计算整个缓冲区大小:

 var data []byte for i := 0; i < 1000; i++ { data = append(data, getShortStringFromSomewhere()...) } return string(data) 

按照我的基准testing ,它比复制解决scheme慢了20%(每附加8.1ns而不是6.72ns),但仍比使用字节快55%。缓冲。

您可以创build一个大的字节片段,并使用string切片将短string的字节复制到其中。 “Effective Go”中有一个function:

 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 ( )将其再次转换为string。

 package main import ( "fmt" ) func main() { var str1 = "string1" var str2 = "string2" out := fmt.Sprintf("%s %s ",str1, str2) fmt.Println(out) } 

扩展cd1的答案:你可以使用append()而不是copy()。 append()使得提前规定更大,花费更多的内存,但节省了时间。 我在你的顶部添加了两个基准 。 用本地运行

 go test -bench=. -benchtime=100ms 

在我的thinkpad T400s上,它产生了:

 BenchmarkAppendEmpty 50000000 5.0 ns/op BenchmarkAppendPrealloc 50000000 3.5 ns/op BenchmarkCopy 20000000 10.2 ns/op 

我原来的build议是

 s12 := fmt.Sprint(s1,s2) 

但上面的答案使用bytes.Buffer – WriteString()是最有效的方法。

我最初的build议使用reflection和types开关。 请参阅(p *pp) doPrint(p *pp) printArg
基本types没有通用的Stringer()接口,就像我天真地想的那样。

至less,Sprint()在内部使用了一个bytes.Buffer。 从而

 `s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)` 

在内存分配方面是可以接受的。

=> Sprint()串联可用于快速debugging输出。
=>否则使用bytes.Buffer … WriteString

@ cd1和其他答案的基准代码是错误的。 bN不应该设置在基准函数中。 它由dynamictesting工具dynamic设置,以确定testing的执行时间是否稳定。

基准函数应该运行相同的testing次数,并且循环内部的testing对于每次迭代应该是相同的。 所以我通过添加一个内部循环来修复它。 我还为其他一些解决scheme添加了基准:

 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 < bN; 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 Intel 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 

结论:

  1. CopyPreAllocate是最快的方法; AppendPreAllocate非常接近No.1,但编写代码更容易。
  2. 对于速度和内存使用情况来说, Concat性能非常差。 不要使用它。
  3. Buffer#WriteBuffer#WriteString速度基本相同,与Dani-Br在评论中所说的相反。 考虑到string确实是Go中的[]byte ,这是有道理的。
  4. bytes.Buffer基本上使用与Copy附加书籍和其他东西相同的解决scheme。
  5. CopyAppend使用64的引导程序大小,与bytes.Buffer相同
  6. Append使用更多的内存和分配,我认为这与它使用的增长algorithm有关。 它不像字节那样快地增长内存。缓冲区

build议:

  1. 对于简单的任务,比如OP想要的,我会使用Append或者AppendPreAllocate 。 它足够快,易于使用。
  2. 如果需要同时读写缓冲区,当然使用bytes.Buffer 。 这就是它的目的。

这是@ cd1( Go 1.8linux x86_64 )提供的基准testing的实际版本,修复了@icza和@PickBoy提到的错误。

Bytes.Buffer只比直接string连接通过+运算符快7倍。

 package performance_test import ( "bytes" "fmt" "testing" ) const ( concatSteps = 100 ) func BenchmarkConcat(b *testing.B) { for n := 0; n < bN; n++ { var str string for i := 0; i < concatSteps; i++ { str += "x" } } } func BenchmarkBuffer(b *testing.B) { for n := 0; n < bN; n++ { var buffer bytes.Buffer for i := 0; i < concatSteps; i++ { buffer.WriteString("x") } } } 

时序:

 BenchmarkConcat-4 300000 6869 ns/op BenchmarkBuffer-4 1000000 1186 ns/op 

看一看golang的strconv库,可以访问几个AppendXX函数,使我们能够将string与string和其他数据types连接起来。

[Off Topic]查看这个博客,了解一些golang的function

从“strings”包中joinstrings.Join()

如果你有一个types不匹配(就像你想join一个int和一个string一样),你可以使用RANDOMTYPE(你想改变的东西)

EX:

 package main import "strings" var intEX = 0 var stringEX = "hello all you " var stringEX2 = " people in here" func main() { strings.Join(stringEX, string(intEX), stringEX2) } 
 s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))