Search code examples
stringgostringbuilder

How to prepend a string in GoLang without plus operation?


This may be a duplicate but I haven't been able to find the correct answer anywhere. How do you prepend to a string in GoLang without using + operator (which is considered slow)?

I know I can append to a string using bytes.Buffer but WriteString only appends. If I want to prepend, I'll have to write to the prefix with the string of suffix like so:

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

Is there a better way?


Solution

  • Using the strings.Builder is the way to go, if it is on a performance critical path, otherwise a simple + is more than good; should you know the length beforehand you may use the array or slice and copy too (Thanks to @mh-cbon, and see 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
    

    Benchmark:

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