如何在go中有效地连接字符串

在Go中,string是一种基本类型,这意味着它是只读的,每次对它的操作都会创建一个新字符串。

因此,如果我想多次连接字符串而不知道结果字符串的长度,那么最好的方法是什么?

幼稚的方式是:

var s stringfor i := 0; i < 1000; i++ {s += getShortStringFromSomewhere()}return s

但这似乎不是很有效。

618273 次浏览

你可以创建一个大的字节切片,并使用字符串切片将短字符串的字节复制到其中。“有效围棋”中给出了一个函数:

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

如果您有一个字符串切片想要有效地转换为字符串,那么您可以使用这种方法。否则,请查看其他答案。

字符串包中有一个名为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.binthis is a joined string

新方法:

从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)时间内完成。

我只是在我自己的代码中对上面发布的顶级答案进行了基准测试(递归树遍历),简单的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 []stringparts = 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,"")}

我最初的建议是

s12 := fmt.Sprint(s1,s2)

但上面的答案使用Bytes. Buffer-WriteString()返回值:是最有效的方法。

我最初的建议使用反射和类型开关。#0和#1
基本类型没有通用的Stringer()接口,正如我天真地认为的那样。

至少,Sprint()内部使用了bytes. Buffer

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

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

=>Sprint()连接可用于快速调试输出。
=>否则使用bytes. Buffer… WriteString

s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))

如果您知道要预分配的字符串的总长度,那么连接字符串的最有效方法可能是使用内置函数copy。如果您不知道手的总长度,请不要使用copy,而是阅读其他答案。

在我的测试中,这种方法比使用bytes.Buffer快约3倍,比使用运算符+快得多(约12,000倍)。此外,它使用的内存更少。

我创建了一个测试用例来证明这一点,以下是结果:

BenchmarkConcat  1000000    64497 ns/op   502018 B/op   0 allocs/opBenchmarkBuffer  100000000  15.5  ns/op   2 B/op        0 allocs/opBenchmarkCopy    500000000  5.39  ns/op   0 B/op        0 allocs/op

下面是测试代码:

package main
import ("bytes""strings""testing")
func BenchmarkConcat(b *testing.B) {var str stringfor 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.Bufferfor 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.10func 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)}}

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

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

通过我的基准,它比复制解决方案慢20%(8.1ns/append而不是6.72ns),但仍比使用bytes. Buffer快55%。

扩展cd1的答案:您可以使用append()而不是Copy()。append()做了越来越大的预付款,消耗了更多的内存,但节省了时间。我在你的顶部添加了两个基准。本地运行

go test -bench=. -benchtime=100ms

在我的thinkpad T400s上,它产生:

BenchmarkAppendEmpty    50000000         5.0 ns/opBenchmarkAppendPrealloc 50000000         3.5 ns/opBenchmarkCopy           20000000        10.2 ns/op
package main
import ("fmt")
func main() {var str1 = "string1"var str2 = "string2"out := fmt.Sprintf("%s %s ",str1, str2)fmt.Println(out)}

strings.Join()来自“字符串”包

如果你有一个类型不匹配(比如如果你试图连接一个int和一个字符串),你做RANDOMTYPE(你想改变的东西)

EX:

package main
import ("fmt""strings")
var intEX = 0var stringEX = "hello all you "var stringEX2 = "people in here"

func main() {s := []string{stringEX, stringEX2}fmt.Println(strings.Join(s, ""))}

输出:

hello all you people in here

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

Bytes.Buffer仅比通过+运算符直接字符串连接快7倍。

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

时间:

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

2018年添加的注释

从Go 1.10开始,有一个strings.Builder类型,请查看此答案以获取更多详细信息

201x之前的答案

@cd1等答案的基准代码错误。b.N不是应该在基准函数中设置的。它由go测试工具动态设置,以确定测试的执行时间是否稳定。

基准测试函数应该运行相同的测试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 stringfor n := 0; n < b.N; n++ {bs := make([]byte, cnt*len(sss))bl := 0for 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 stringfor 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 stringfor 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 stringfor n := 0; n < b.N; n++ {data := make([]byte, 0, 64) // same size as bootstrap array of bytes.Bufferfor 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 stringfor 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 stringfor n := 0; n < b.N; n++ {var buf bytes.Bufferfor 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 stringfor n := 0; n < b.N; n++ {var buf bytes.Bufferfor 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 stringfor n := 0; n < b.N; n++ {var str stringfor 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英特尔酷睿i7

测试结果:

BenchmarkCopyPreAllocate-8         20000             84208 ns/op          425984 B/op          2 allocs/opBenchmarkAppendPreAllocate-8       10000            102859 ns/op          425984 B/op          2 allocs/opBenchmarkBufferPreAllocate-8       10000            166407 ns/op          426096 B/op          3 allocs/opBenchmarkCopy-8                    10000            160923 ns/op          933152 B/op         13 allocs/opBenchmarkAppend-8                  10000            175508 ns/op         1332096 B/op         24 allocs/opBenchmarkBufferWrite-8             10000            239886 ns/op          933266 B/op         14 allocs/opBenchmarkBufferWriteString-8       10000            236432 ns/op          933266 B/op         14 allocs/opBenchmarkConcat-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确实是[]byte,这是有道理的。
  4. bytes. Buffer基本上使用与Copy相同的解决方案,额外记账和其他东西。
  5. CopyAppend使用的引导大小为64,与bytes. Buffer相同
  6. Append使用更多的内存和allocs,我觉得和它使用的增长算法有关,内存增长不如bytes快

操作建议:

  1. 对于像OP想要的简单任务,我会使用AppendAppendPreAllocate。它足够快且易于使用。
  2. 如果需要同时读取和写入缓冲区,当然使用bytes.Buffer。这就是它的设计目的。

在Go 1.10+中,有strings.Builder这里

生成器用于使用写入方法有效地构建字符串。它最大限度地减少内存复制。零值已准备好使用。


示例

这与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类型。


与bytes. Buffer的区别

  • 它只能增长或重置。

  • 它有一个内置的CopyCheck机制,可以防止意外复制它:

    func (b *Builder) copyCheck() { ... }

  • bytes.Buffer中,可以像这样访问底层字节:(*Buffer).Bytes()

    • strings.Builder避免了这个问题。
    • 有时,这不是问题,而是需要的。
    • 例如:用于将字节传递给#0等时的偷看行为。
  • bytes.Buffer.Reset()倒带和重用底层缓冲区,而strings.Builder.Reset()不要,它分离缓冲区。


说明

  • 不要复制StringBuilder值,因为它缓存基础数据。
  • 如果要共享StringBuilder值,请使用指向它的指针。

查看其源代码以获取更多详细信息,在这里

我使用以下方法:-

package main
import ("fmt""strings")
func main (){concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator.fmt.Println(concatenation) //abc}
package main
import ("fmt")
func main() {var str1 = "string1"var str2 = "string2"result := make([]byte, 0)result = append(result, []byte(str1)...)result = append(result, []byte(str2)...)result = append(result, []byte(str1)...)result = append(result, []byte(str2)...)
fmt.Println(string(result))}

使用内存分配统计数据进行基准测试结果。检查github处的基准代码。

使用string. Builder优化性能。

go test -bench . -benchmemgoos: darwingoarch: amd64pkg: github.com/hechen0/goexp/expsBenchmarkConcat-8                1000000             60213 ns/op          503992 B/op          1 allocs/opBenchmarkBuffer-8               100000000               11.3 ns/op             2 B/op          0 allocs/opBenchmarkCopy-8                 300000000                4.76 ns/op            0 B/op          0 allocs/opBenchmarkStringBuilder-8        1000000000               4.14 ns/op            6 B/op          0 allocs/opPASSok      github.com/hechen0/goexp/exps   70.071s

 func JoinBetween(in []string, separator string, startIndex, endIndex int) string {if in == nil {return ""}
noOfItems := endIndex - startIndex
if noOfItems <= 0 {return EMPTY}
var builder strings.Builder
for i := startIndex; i < endIndex; i++ {if i > startIndex {builder.WriteString(separator)}builder.WriteString(in[i])}return builder.String()}

简单易懂的解决方案。详细信息在评论中。复制覆盖切片的元素。我们正在切片单个元素并覆盖它。

package main
import ("fmt")
var N int = 100000
func main() {slice1 := make([]rune, N, N)//Efficient with fast performance, Need pre-allocated memory//We can add a check if we reached the limit then increase capacity//using append, but would be fined for data copying to new array. Also append happens after the length of current slice.for i := 0; i < N; i++ {copy(slice1[i:i+1], []rune{'N'})}fmt.Println(slice1)
//Simple but fast solution, Every time the slice capacity is reached we get a fine of effort that goes//in copying data to new arrayslice2 := []rune{}for i := 0; i <= N; i++ {slice2 = append(slice2, 'N')}fmt.Println(slice2)
}