协慌网

登录 贡献 社区

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

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

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

天真的方法是:

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

但这似乎不是很有效。

答案

新的方法:

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

旧方法:

使用bytes包。它具有实现io.Writer Buffer类型。

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

在 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 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可以防止出现此问题。
    • 有时,这不是问题,而是需要的。
    • 例如:对于将字节传递到io.Reader等时的偷窥行为。
  • bytes.Buffer.Reset()倒带并重用基础缓冲区,而strings.Builder.Reset()则不,它分离缓冲区。


笔记

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

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

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

在我的测试中,这种方法比使用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)
    }
}