在Go中,a string
是原始类型,这意味着它是只读的,并且对它的每次操作都将创建一个新字符串.
因此,如果我想在不知道结果字符串长度的情况下多次连接字符串,那么最好的方法是什么?
天真的方式是:
s := "" 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)时间内完成的.
连接字符串的最有效方法是使用内置函数copy
.在我的测试中,这种方法比使用方法快3倍,比使用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) } }
从Go 1.10开始strings.Builder
,这里有一个.
Builder用于使用Write方法高效地构建字符串.它最小化了内存复制.零值可以使用.
用法:
它几乎一样bytes.Buffer
.
package main import ( "strings" "fmt" ) func main() { var str strings.Builder for i := 0; i < 1000; i++ { str.WriteString("a") } fmt.Println(str.String()) }
它支持的StringBuilder方法和接口:
它的方法是在考虑现有接口的情况下实现的,因此您可以在代码中轻松切换到新的Builder.
Grow(int)- > bytes.Buffer #Erow
Len()int - > bytes.Buffer #Len
Reset()- > bytes.Buffer #Reset
String()string - > fmt.Stringer
写([] byte)(int,error)- > io.Writer
WriteByte(byte)错误- > io.ByteWriter
WriteRune(rune)(int,error)- > bufio.Writer#WriteRune - bytes.Buffer#WriteRune
WriteString(string)(int,error)- > io.stringWriter
零值使用:
var buf strings.Builder
与bytes.Buffer的差异:
它只能增长或重置.
在bytes.Buffer
底层字节可以像这样逃避:(*Buffer).Bytes()
; strings.Builder
防止这个问题.
它还有一个copyCheck机制,可以防止意外复制它(io.Reader
).
在这里查看其源代码.
字符串包中有一个库函数,名为Join
:http:
//golang.org/pkg/strings/#Join
看一下代码Join
显示类似于Append函数的方法Kinopiko写道:https://golang.org/src/strings/strings.go#L420
用法:
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
我只是在我自己的代码(递归树步行)中对上面发布的最佳答案进行了基准测试,而简单的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.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,"") }
您可以创建一大块字节,并使用字符串切片将短字符串的字节复制到其中."Effective Go"中有一个功能:
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 ( )
大块字节将其再次转换为字符串.
这是最快的解决方案,不需要您首先了解或计算总体缓冲区大小:
var data []byte for i := 0; i < 1000; i++ { data = append(data, getShortStringFromSomewhere()...) } return string(data)
根据我的基准测试,它比复制解决方案慢20%(每次追加8.1ns而不是6.72ns),但仍然比使用bytes.Buffer快55%.
package main import ( "fmt" ) func main() { var str1 = "string1" var str2 = "string2" out := fmt.Sprintf("%s %s ",str1, str2) fmt.Println(out) }
从Go 1.10开始,strings.Builder
建议替代b.N
.检查1.10发行说明
新类型的Builder是bytes.Buffer的替代,用于将文本累积到字符串结果中的用例.Builder的API是bytes.Buffer的受限子集,它允许它安全地避免在String方法期间制作数据的副本.
================================================== ==========
@ cd1和其他答案的基准代码是错误的.b.N
不应该在基准函数中设置.它由go测试工具动态设置,以确定测试的执行时间是否稳定.
基准函数应该运行相同的测试CopyPreAllocate
时间,并且循环内的测试对于每次迭代应该是相同的.所以我通过添加内循环来修复它.我还为其他一些解决方案添加了基准:
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 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
结论:
AppendPreAllocate
是最快的方式; Concat
非常接近No.1,但编写代码更容易.
Buffer#Write
在速度和内存使用方面都表现不佳.不要使用它.
Buffer#WriteString
与string
Dani-Br在评论中所说的相反,速度基本相同.考虑到Go []byte
确实Copy
存在,这是有道理的.
bytes.Buffer基本上使用Copy
与额外的簿记和其他东西相同的解决方案.
Append
并Append
使用64的引导程序大小,与bytes.Buffer相同
Append
使用更多的内存和分配,我认为它与它使用的增长算法有关.它不像bytes.Buffer那样快速增长内存
建议:
对于OP想要的简单任务,我会使用AppendPreAllocate
或bytes.Buffer
.它足够快且易于使用.
如果需要同时读取和写入缓冲区,请使用strings.Builder
.这就是它的设计目标.
我原来的建议是
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
扩展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