如何在没有加号操作的情况下在 GoLang 中添加一个字符串?

How to prepend a string in GoLang without plus operation?

这可能是重复的,但我无法在任何地方找到正确的答案。如何在不使用 + 运算符(这被认为很慢)的情况下在 GoLang 中添加字符串?

我知道我可以使用 bytes.Buffer 追加 到字符串,但 WriteString 只能追加。如果我想 prepend,我将不得不像这样用后缀的字符串写入前缀:

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var b bytes.Buffer 
      
    b.WriteString("W") 
    b.WriteString("o") 
    b.WriteString("r") 
    b.WriteString("l") 
    b.WriteString("d") 
   
    var a bytes.Buffer
    a.WriteString("Hello ")
    a.WriteString(b.String())
  
    fmt.Println(a.String()) 
}

有没有更好的方法?

使用 strings.Builder 是可行的方法,如果它在 性能关键路径 上,否则 简单 + 不止于此;如果您事先知道长度,您也可以使用 数组或切片和复制 (感谢 @mh-cbon,请参阅 slice-allocation-performance):

go test -benchtime=4731808x -benchmem -bench .
BenchmarkBufferArraySimplified-8  4731808   11.36 ns/op   0 B/op  0 allocs/op
BenchmarkBufferArray-8            4731808   62.19 ns/op   0 B/op  0 allocs/op
BenchmarkBuilder-8                4731808  140.7 ns/op   32 B/op  3 allocs/op
BenchmarkBuffer-8                 4731808  200.7 ns/op  128 B/op  2 allocs/op
BenchmarkByteAppend-8             4731808  223.2 ns/op   16 B/op  4 allocs/op
BenchmarkPlus-8                   4731808  226.2 ns/op   16 B/op  4 allocs/op
BenchmarkByteCopy-8               4731808  254.1 ns/op   32 B/op  5 allocs/op
BenchmarkPrepend-8                4731808  273.7 ns/op   24 B/op  5 allocs/op

基准:

package main

import (
    "bytes"
    "strings"
    "testing"
)

func BenchmarkBufferArraySimplified(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var y [11]byte // should you know the length beforehand
        y[6] = 'W'
        y[7] = 'o'
        y[8] = 'r'
        y[9] = 'l'
        y[10] = 'd'

        copy(y[0:], "Hello ")
        _ = string(y[:])
    }
}
func BenchmarkBufferArray(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var y [11]byte
        hello := "Hello "
        n := len(hello) // should you know the length beforehand
        b := bytes.NewBuffer(y[:n])
        b.WriteString("W")
        b.WriteString("o")
        b.WriteString("r")
        b.WriteString("l")
        b.WriteString("d")

        a := bytes.NewBuffer(y[:0])
        a.WriteString(hello) // prepend
        _ = b.String()
    }
}
func BenchmarkBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var b strings.Builder
        b.WriteString("W")
        b.WriteString("o")
        b.WriteString("r")
        b.WriteString("l")
        b.WriteString("d")

        var a strings.Builder
        a.WriteString("Hello ") // prepend
        a.WriteString(b.String())
        _ = a.String()
    }
}
func BenchmarkBuffer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var b bytes.Buffer
        b.WriteString("W")
        b.WriteString("o")
        b.WriteString("r")
        b.WriteString("l")
        b.WriteString("d")

        var a bytes.Buffer
        a.WriteString("Hello ") // prepend
        a.WriteString(b.String())
        _ = a.String()
    }
}
func BenchmarkByteAppend(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := "W"
        b += "o"
        b += "r"
        b += "l"
        b += "d"

        _ = ByteAppend("Hello ", b) // prepend
    }
}
func ByteAppend(a, b string) string {
    return string(append([]byte(a), []byte(b)...)) // a+b
}
func BenchmarkPlus(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := "W"
        b += "o"
        b += "r"
        b += "l"
        b += "d"

        _ = "Hello " + b // prepend
    }
}
func BenchmarkByteCopy(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := "W"
        b += "o"
        b += "r"
        b += "l"
        b += "d"

        _ = byteCopy("Hello ", b) // prepend
    }
}
func byteCopy(a, b string) string {
    c := make([]byte, len(a)+len(b))
    copy(c, a)
    copy(c[len(a):], b) // a+b
    return string(c)
}
func BenchmarkPrepend(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := " "
        b += "W"
        b += "o"
        b += "r"
        b += "l"
        b += "d"

        _ = string(prepend([]byte(b), []byte("Hello"))) // prepend
    }
}

// prepend: insert b into a at index 0:  len(a) >= len(b)
func prepend(a, b []byte) []byte {
    // if len(a) >= len(b) {
    a = append(a[:len(b)], a...) // grow
    copy(a, b)
    return a
    // }
    // return append(b, a...)
}

我 运行 遇到需要在前面添加 10,000 个字符串的情况。这与前置 10 个字符串的最快方法有点不同,但此处尚未得到解答。

TL;DR 将 10,000 个字符串存储在 []string 中,然后使用 strings.StringBuilder 到 assemble 倒序排列。

        var strs []string
        for i := 0; i < 10000; i++ {
            strs = append(strs, "Put your string here")
        }
        var b strings.Builder
        for i := len(strs) - 1; i >= 0; i -= 1 {
            b.WriteString(strs[j])
        }

+ 的天真方法使用了 10 GB 的内存 + 1 秒。将相当多的时间投入到从后面填充缓冲区的自定义前置代码中,将相同的测试用例减少到 10mb + 2 毫秒。但事实证明,只需存储每个字符串,然后使用字符串构建器就可以用更少的代码实现几乎相同的结果。 (14mb 2.4 毫秒)。 这是基准测试结果:

go test -benchtime=50x -benchmem -bench .
BenchmarkPrepend10kAddition-20                                50         971398162 ns/op        9666950277 B/op    20325 allocs/op
BenchmarkPrepend10kPrepender-20                               50           1924394 ns/op        10165544 B/op      29913 allocs/op
BenchmarkPrepend10kReorderThenStringBuilder-20                50           2436740 ns/op        14518100 B/op      19952 allocs/op

这是代码(没有前缀 class 因为需要更多的工作才能 public 准备好):

package main

import (
    "strconv"
    "strings"
    "testing"

    "github.com/stretchr/testify/assert"
)

var teststrings []string = []string{
    "Test test tses test test sets etsetsetsetsettesestsetet\n",
    "TEST TEST TEST TEST TEST EST SET ET SET EST EST SET E TSE TES T STE ET SETT\n",
    "ashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",    "asdhakjsdhaksjdhdskajhsdajkshdkajshdkjasdkjdhasfhjadshfashdasgfkjashdgfjhsadgfjashgdfkjhasgdfkjhgasdfkjghasdfkjhgasdkjfhgasdkjghfasdkjghfasdhjfgasjkghfsadhjgfaashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",
    "asdhakjsdhaksjdhdskajhsdajkshdkajshdkjasdkjdhasfhjadshfashdasgfkjashdgfjhsadgfjashgdfkjhasgdfkjhgasdfkjghasdfkjhgasdkjfhgasdkjghfasdkjghfasdhjfgasjkghfsadhjgfaashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",
    "asdhakjsdhaksjdhdskajhsdajkshdkajshdkjasdkjdhasfhjadshfashdasgfkjashdgfjhsadgfjashgdfkjhasgdfkjhgasdfkjghasdfkjhgasdkjfhgasdkjghfasdkjghfasdhjfgasjkghfsadhjgfaashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",
    "asdhakjsdhaksjdhdskajhsdajkshdkajshdkjasdkjdhasfhjadshfashdasgfkjashdgfjhsadgfjashgdfkjhasgdfkjhgasdfkjghasdfkjhgasdkjfhgasdkjghfasdkjghfasdhjfgasjkghfsadhjgfaashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",
    "asdhakjsdhaksjdhdskajhsdajkshdkajshdkjasdkjdhasfhjadshfashdasgfkjashdgfjhsadgfjashgdfkjhasgdfkjhgasdfkjghasdfkjhgasdkjfhgasdkjghfasdkjghfasdhjfgasjkghfsadhjgfaashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",
    "asdhakjsdhaksjdhdskajhsdajkshdkajshdkjasdkjdhasfhjadshfashdasgfkjashdgfjhsadgfjashgdfkjhasgdfkjhgasdfkjghasdfkjhgasdkjfhgasdkjghfasdkjghfasdhjfgasjkghfsadhjgfaashkajshfksjhdfksjdhfklhjasdfkljhasdfljashdfkljahsdfkjshafdkjashdflkjhsdgkljhsdfklgjhsklfjghksdfjgkldsjfghldksjhfgkdsfjghkdsjhfgkldjsghkldsjfghdklsfgasdjhaksdhjaksdjhasdkjhfksdjhfkjashfdjldhjasfkljashdfkljsahdflkjashdfldhjasfkjsdhf\n",
}

func BenchmarkPrepend10kAddition(b *testing.B) {
    for i := 0; i < b.N; i++ {
        str := ""
        for j := 0; j < 10000; j++ {
            str = (strconv.Itoa(j) + teststrings[j%4]) + str
        }
        _ = string(str)
    }
}

func BenchmarkPrepend10kPrepender(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // Example using custom prepender code not included in this post
        prepender := NewStringPrepender()
        for j := 0; j < 10000; j++ {
            prepender.Prepend(strconv.Itoa(j) + teststrings[j%4])
        }
        _ = string(prepender.String())
    }
}

func BenchmarkPrepend10kReorderThenStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // What if we have 10,000 strings and assemble them at the end?
        var strs []string
        for j := 0; j < 10000; j++ {
            strs = append(strs, strconv.Itoa(j)+teststrings[j%4])
        }
        var b strings.Builder
        for j := len(strs) - 1; j >= 0; j -= 1 {
            b.WriteString(strs[j])
        }
        _ = string(b.String())
    }
}

为了在基准测试中生成字符串,我使用了 Itoa + 测试字符串列表。这可能是不必要的,但我添加它们是为了确保 []string 中的值不会指向相同的内存位置。 运行domish 测试字符串会改变连接字符串的长度。