如何在Go中有效地串联字符串


727

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

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

天真的方法是:

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

但这似乎不是很有效。


7
另外一个替补席
伊万·布莱克

1
注意:这个问题和大多数答案似乎是在append()进入该语言之前写的,这是一个很好的解决方案。它的执行速度将非常快,copy()但即使容量不足也将首先分配切片,即使这意味着要分配新的后备阵列。 bytes.Buffer如果您希望使用它的其他便捷方法,或者您使用的软件包期望它,它仍然有意义。
thomasrutter

7
它不只是“似乎效率很低”;它有一个特定的问题,即我们刚开始工作的头几周就会遇到我们遇到的每位新的非CS员工。这是二次方-O(n * n)。考虑一下数字顺序:1 + 2 + 3 + 4 + ...。这是n*(n+1)/2,基地的三角形的面积n。在循环中附加不可变字符串时,分配大小1,然后大小2,然后大小3,依此类推。这种二次资源消耗不仅以这种方式体现出来。
罗布

Answers:


856

新方法:

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


24
而不是println(string(buffer.Bytes())); 使用可能只是做println(buffer.String())
FigmentEngine '02

26
相反buffer := bytes.NewBufferString(""),您可以这样做var buffer bytes.Buffer。您也不需要任何分号:)。
crazy2be 2012年

66
快得惊人。使我的程序中一些幼稚的“ +”字符串concat从3分钟变为1.3
马尔科姆

10
+1为“ O(n)时间”;我认为重要的是要发表更多这样的评论。
2014年

8
Go 1.10添加了strings.Builder,类似于bytes.Buffer,但是当最终目标是字符串时,速度更快。
Josh Bleecher Snyder

272

连接字符串的最有效方法是使用内置函数copy。在我的测试中,该方法比使用速度快约3倍,比使用bytes.Buffer运算符快得多(约12,000倍)+。同样,它使用更少的内存。

我创建了一个测试案例来证明这一点,结果如下:

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

6
bytes.Buffer应该与副本基本相同(我猜有一些额外的簿记),并且速度没有什么不同。所以我会用:)。区别在于缓冲区以0字节开头,因此必须重新分配(我猜这似乎有点慢)。不过更容易使用。
阿克套2014年

5
buffer.Write(字节)比快30%buffer.WriteString。[如果您能以以下方式获取数据很有用[]byte]
Dani-Br

34
请注意,基准测试结果失真并且不是真实的。不同的基准函数将使用的不同值调用b.N,因此您无需比较要执行的同一任务的执行时间(例如,一个函数可能附加1,000字符串,另一个函数可能附加字符串,这可能10,000对平均值产生很大影响)BenchmarkConcat()例如,附加时间为1 )。您应在每种情况下都使用相同的追加计数(一定不能使用b.N),并for在to 的范围内进行所有串联b.N(即,for嵌入2个循环)。
icza

18
此外,通过显式忽略分配所花费的时间(包括在其他基准中),可以使复制基准产生偏差。
gha.st,2015年

6
另外,复制基准测试依赖于知道结果字符串的长度。
斯卡洛特'16

227

在Go 1.10+中strings.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 str strings.Builder

    for i := 0; i < 1000; i++ {
        str.WriteString("a")
    }

    fmt.Println(str.String())
}

单击以在操场上查看


注意

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

支持的接口

正在考虑现有接口的情况下实现StringBuilder的方法。这样您就可以在代码中轻松切换到新的Builder类型。


与bytes.Buffer的差异

  • 它只能增长或重置。

  • 它具有内置的copyCheck机制,可防止意外复制它:

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

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

    • strings.Builder 可以防止此问题。
    • 有时,这不是问题,而是需要的。
    • 例如:对于将字节传递给io.Readeretc 时的偷窥行为。

在此处查看其源代码以获取更多详细信息


5
“逃脱”是什么意思?您是说要在字符串中转义,还是可以公开底层字节?
makhdumi

1
@makhdumi是的,第二,底层字节的暴露。
伊南克·古姆斯

值得一提的是,strings.Builder它使用指针接收器来实现其方法,这让我有些犹豫。结果,我可能会使用创建一个new
邓肯·琼斯,

@DuncanJones我添加了一个注释,因为它主要用于缓存数据,因此在函数之间共享它时,通常使用指向它的指针。在同一函数中,您也可以将其用作非指针。
伊南克·古姆斯

130

字符串包中有一个库函数,名为Joinhttp : //golang.org/pkg/strings/#Join

看看的代码,可以看到Join类似于附录功能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

21
当您必须遍历不是[]字符串的内容时,该功能将不起作用。
马尔科姆

42

我只是在自己的代码(递归树遍历)中对上面发布的最佳答案进行了基准测试,而简单的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
我相信OP更加关注内存复杂度,而不是运行时复杂度,因为天真的字符串连接每次都会导致新的内存分配。
galaktor 2012年

15
速度较慢可能与使用fmt.Fprint而不是buffer.WriteString("\t"); buffer.WriteString(subs[i]);
Robert Jack Will

我很高兴地知道,从这句话说,我最喜欢(strings.Join)的最快跑步方法就是胜利!(bytes.Buffer)
Chetabahana

23

您可以创建一个大的字节切片,然后使用字符串切片将短字符串的字节复制到其中。“有效执行”中提供了一个功能:

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


有趣的是,Go中有很多方法可以做到这一点。
伊扎克

11
实际上,它还说这个主意是如此有用,以至于它被内置了。因此,您可以将替换为append(slice, byte...)
阿克套2014年

23

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

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

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


23
package main

import (
  "fmt"
)

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

2
欢迎使用Stack Overflow!花点时间通读帮助中心中的编辑帮助。堆栈溢出上的格式与其他站点不同。
Rizier123 '16

2
尽管此代码段可以解决问题,但提供说明确实有助于提高您的帖子质量。请记住,您将来会为读者回答这个问题,而这些人可能不知道您提出代码建议的原因。也请尽量不要在代码中加入解释性注释,这会降低代码和解释的可读性!
Rizier123 '16

简单的解决方案👍–
Finn

22

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 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#Write并且Buffer#WriteString基本上都是在速度相同,相反的是@达尼-溴在注释中说。考虑string确实[]byte在Go中是有意义的。
  4. bytes.Buffer基本上使用Copy与额外簿记和其他内容相同的解决方案。
  5. CopyAppend使用引导大小为64,与bytes.Buffer相同
  6. Append使用更多的内存和分配,我认为这与其使用的增长算法有关。它没有像字节一样快地增长内存。

建议:

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

13

我最初的建议是

s12 := fmt.Sprint(s1,s2)

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

我最初的建议是使用反射和类型开关。(p *pp) doPrint(p *pp) printArg
有没有通用斯金格()接口,基本类型,因为我曾天真地以为。

至少,尽管Sprint()在内部使用bytes.Buffer。从而

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

就内存分配而言,是可以接受的。

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


8
它不是内置的,效率也不高。
peterSO

导入软件包(如fmt)意味着它不是内置的。它在标准库中。
马尔科姆

之所以慢,是因为它在其参数上使用了反射。很有效。否则,它不是比strings.Join加盟效率较低
ithkuil

11

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

4

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

Bytes.Buffer7比通过+运算符的直接字符串连接快几倍。

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

我认为手动设置bN不是使用测试包基准测试功能的正确方法
PickBoy

@PickBoy,请说明您的观点。您为什么认为b.N是公共变量?
Vitaly Isaev

1
bN不应在基准功能中设置。它由go测试工具动态设置。一个基准函数应该运行相同的测试bN次,但是在您的代码(以及@ cd1的代码)中,循环中的每个测试都是一个不同的测试(因为字符串的长度在增加)
PickBoy

@PickBoy,如果您b.N动态设置go测试工具,则会在不同的测试用例中使用不同长度的字符串。查看评论
Vitaly Isaev

这就是为什么您应该在bN循环内添加一个固定循环次数(例如10000)的内部循环。
PickBoy

3

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

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
}

这没有解决OP通过for循环通过一系列迭代来构建字符串的问题。
codeforester

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

3
请不要只发布代码答案。请解释一下此代码的作用以及解决方案的原因。
Korashen

-1

具有内存分配统计信息的基准测试结果。检查github上的基准代码。

使用strings.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

请将@ cd1记入您在此处构建的原始测试用例中。
colm.anseo

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

5
这是非常慢的解决方案,因为它使用反射,它分析格式字符串,并为[]byte(s1)转换创建数据副本。将其与发布的其他解决方案进行比较,您能否说出解决方案的一项优势?
pts

-5

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

如果类型不匹配(例如,如果您尝试连接一个int和一个字符串),则执行RANDOMTYPE(要更改的内容)

例如:

package main

import (
    "fmt"
    "strings"
)

var intEX = 0
var 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

4
这段代码甚至没有编译:strings.Join()仅接受两个参数:slice和Separator string
icza

这无济于事
Anshu

在此处添加一些更改。
安舒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.