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

时间:2009-11-19 03:44:10

标签: string go string-concatenation

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

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

天真的方式是:

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

但这看起来效率不高。

20 个答案:

答案 0 :(得分:777)

注释于2018年添加

从Go 1.10开始,有一个strings.Builder类型,please take a look at this answer for more detail

201x之前的回答

最好的方法是使用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)时间内完成的。

答案 1 :(得分:260)

连接字符串的最有效方法是使用内置函数copy。在我的测试中,这种方法比使用bytes.Buffer快3倍,比使用运算符+快得多(~12,000x)。此外,它使用更少的内存。

我已创建a test case来证明这一点,结果如下:

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

答案 2 :(得分:125)

字符串包中有一个名为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

答案 3 :(得分:123)

从Go 1.10开始,有 strings.Builder here

  

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方法和接口:

它的方法正在考虑现有接口,以便您可以在代码中轻松切换到新的构建器。

零值使用:

var buf strings.Builder

与bytes.Buffer:

的差异
  • 它只能增长或重置。

  • bytes.Buffer基础字节可以像这样转义:(*Buffer).Bytes(); strings.Builder可以防止出现此问题。有时候,这不是问题而是需要(例如,当字节传递给io.Reader等时偷看行为)。

  • 它还内置了copyCheck机制,可以防止意外复制它(func (b *Builder) copyCheck() { ... })。

查看其源代码here

答案 4 :(得分:39)

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

答案 5 :(得分:21)

您可以创建一大块字节,并使用字符串切片将短字符串的字节复制到其中。 “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 ( )将其再次转换为字符串。

答案 6 :(得分:20)

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

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

通过我的benchmark,它比复制解决方案慢20%(每个8.1ns) 追加而不是6.72ns)但仍然比使用bytes.Buffer快55%。

答案 7 :(得分:20)

注释于2018年添加

从Go 1.10开始,有一个strings.Builder类型,please take a look at this answer for more detail

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 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

结论:

  1. CopyPreAllocate是最快的方式; AppendPreAllocate非常接近No.1,但编写代码更容易。
  2. Concat在速度和内存使用方面都表现不佳。不要使用它。
  3. Buffer#WriteBuffer#WriteString的速度基本相同,与@ Dani-Br在评论中所说的相反。在Go中考虑string确实是[]byte,这是有道理的。
  4. bytes.Buffer基本上使用与Copy相同的解决方案,以及额外的簿记和其他内容。
  5. CopyAppend使用的引导程序大小为64,与bytes.Buffer相同
  6. Append使用更多内存和分配,我认为它与它使用的增长算法有关。它没有像bytes.Buffer那样快速增长内存。
  7. 建议:

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

答案 8 :(得分:17)

package main

import (
  "fmt"
)

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

答案 9 :(得分:12)

我原来的建议是

s12 := fmt.Sprint(s1,s2)

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

我的初步建议使用反射和类型切换。 See (p *pp) doPrint and (p *pp) printArg
基本类型没有通用的Stringer()接口,正如我天真地想的那样。

至少,Sprint()内部使用bytes.Buffer。因此

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

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

=&GT; Sprint()连接可用于快速调试输出 =&GT;否则使用bytes.Buffer ... WriteString

答案 10 :(得分:10)

扩展cd1的答案: 您可以使用append()而不是copy()。 append()会提供更大的预付款,花费更多的内存,但节省时间。 我在你的顶部添加了two more benchmarks。 使用

在本地运行
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

答案 11 :(得分:2)

这是@ 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 string
        for i := 0; i < concatSteps; i++ {
            str += "x"
        }
    }
}

func BenchmarkBuffer(b *testing.B) {
    for n := 0; n < b.N; 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

答案 12 :(得分:1)

goutils.JoinBetween

 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()
}

答案 13 :(得分:1)

我使用以下方法: -

package main

import (
    "fmt"
    "strings"
)

func main (){
    concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator. 
    fmt.Println(concatenation) //abc
}

答案 14 :(得分:0)

对于那些来自Java世界我们StringBuilder进行有效字符串连接的人来说,似乎最新的go版本具有等价的,它被称为Builderhttps://github.com/golang/go/blob/master/src/strings/builder.go

答案 15 :(得分:0)

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

答案 16 :(得分:-1)

查看golang的strconv库,它可以访问几个AppendXX函数,使我们能够将字符串与其他数据类型连接起来。

答案 17 :(得分:-1)

基准测试结果与内存分配统计信息。在github处检查基准代码。

使用字符串。Builder优化性能。

go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/hechen0/goexp/exps
BenchmarkConcat-8                1000000             60213 ns/op          503992 B/op          1 allocs/op
BenchmarkBuffer-8               100000000               11.3 ns/op             2 B/op          0 allocs/op
BenchmarkCopy-8                 300000000                4.76 ns/op            0 B/op          0 allocs/op
BenchmarkStringBuilder-8        1000000000               4.14 ns/op            6 B/op          0 allocs/op
PASS
ok      github.com/hechen0/goexp/exps   70.071s

答案 18 :(得分:-2)

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

答案 19 :(得分:-5)

来自“strings”包的

strings.Join()

如果您的类型不匹配(例如,如果您尝试加入int和字符串),则执行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)
}