String concatenation in Go

This article explores a few different ways to concatenate strings in Golang and gives the pros and cons of each one.

Strings are one of the building blocks of any programming language, and joining two or more strings together is an operation that you might find yourself performing often. There are several possible methods for doing this in Go, each with their own unique tradeoffs.

The methods of concatenating strings described below are ordered from slowest to fastest. But that’s not the only thing to consider when choosing an optimal solution as you’ll see. The concluding part of this article provides a benchmark that you can run locally.

Using the + operator

The easiest way to concatenate strings in Go is by using the concatenation operator (+). Here’s an example:

func main() {
	s := "Hello"
	s += " world!"
	fmt.Println(s) // Hello world!
}

For basic concatenation operations such as the one shown above, this might suffice. However, it’s very inefficient for larger operations such concatenating several strings in a loop. Because strings are immutable data structures, each concatenation using the + operator creates an entirely new string in memory.

To avoid these memory allocations while building up the final string, the strings.Builder type along with its WriteString method can be used instead.

Using strings.Builder

This type was introduced to enable more efficient string building by minimising memory allocations. Here’s the most basic example of using strings.Builder:

func main() {
	slice := []string{"I", " am", " not", " who", " you", " think", " I", " am"}
	var b strings.Builder
	for _, v := range slice {
		_, err := b.WriteString(v)
		if err != nil {
			log.Fatal(err)
		}
	}

	fmt.Println(b.String()) // I am not who you think I am
}

The WriteByte and WriteRune methods are also available in addition to WriteString if you’re building a string by appending one character at a time.

func main() {
	slice := []rune{'H', 'e', 'l', 'l', 'o'}
	var b strings.Builder
	for _, v := range slice {
		_, err := b.WriteRune(v)
		if err != nil {
			fmt.Println(err)
		}

	}

	fmt.Println(b.String()) // Hello
}

You don’t even need to create a new Builder type each time you want to concatenate a string. You can use the Reset method to empty a Builder before building a new string.

func main() {
	slice := []rune{'H', 'e', 'l', 'l', 'o'}
	var b strings.Builder
	for _, v := range slice {
		b.WriteRune(v)
	}

	fmt.Println(b.String()) // Hello
	b.Reset()

	slice = []rune{'G', 'o', 'o', 'd', 'b', 'y', 'e'}
	for _, v := range slice {
		b.WriteRune(v)
	}
	fmt.Println(b.String()) // Goodbye
}

If you want to share a Builder value, use a pointer instead of copying the data. If you try to copy a strings.Builder and try to Write to it, your program will panic!

package main

import "strings"

func main() {
	var b1 strings.Builder
	b1.WriteString("abc")
	b2 := b1
	b2.WriteString("xyz") // panic: strings: illegal use of non-zero Builder copied by value
}

When compared to the primitive concatenation operator, using strings.Builder averaged about 60 times faster on my machine for 1000 concatenation operations. This number will grow if the number of operations increases.

Using bytes.Buffer

You can also use the bytes.Buffer type for efficient string concatenation. It’s usage is similar to strings.Builder and performance is close enough, ever so slightly faster in my experience.

func main() {
	var b bytes.Buffer
	for i := 0; i < 1000; i++ {
		b.WriteString("random")
	}

	fmt.Println(b.String())
}

Appending to a byte slice

Appending to a byte slice has more or less the same performance as using bytes.Buffer, according to my testing.

func main() {
	var b []byte
	s := "random"
	for i := 0; i < 1000; i++ {
		b = append(b, s...)
	}

	fmt.Println(string(b))
}

Wrap up

Here’s a benchmark that you can run locally to see the performance difference between the four methods described above:

package main

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

var s1 = "random"

const LIMIT = 1000

func BenchmarkConcatenationOperator(b *testing.B) {
	var q string
	for i := 0; i < b.N; i++ {
		for j := 0; j < LIMIT; j++ {
			q = q + s1
		}
		q = ""
	}
	b.ReportAllocs()
}

func BenchmarkStringBuilder(b *testing.B) {
	var q strings.Builder
	for i := 0; i < b.N; i++ {
		for j := 0; j < LIMIT; j++ {
			q.WriteString(s1)
		}
		q.Reset()
	}
	b.ReportAllocs()
}

func BenchmarkBytesBuffer(b *testing.B) {
	var q bytes.Buffer
	for i := 0; i < b.N; i++ {
		for j := 0; j < LIMIT; j++ {
			q.WriteString(s1)
		}
		q.Reset()
	}
	b.ReportAllocs()
}

func BenchmarkByteSlice(b *testing.B) {
	var q []byte
	for i := 0; i < b.N; i++ {
		for j := 0; j < LIMIT; j++ {
			q = append(q, s1...)
		}
		q = nil
	}
	b.ReportAllocs()
}

And here are the results of running the benchmark on my machine:

Screenshot of benchmark results

Appending to a byte slice was only slightly faster that writing to a buffer of bytes (it goes back and forth in my experience) which itself was quicker than using strings.Builder. The concatenation operator was by far the slowest with bad memory performance too. I’d steer clear except for really basic cases where performance is not an issue.

The Builder type is probably the best bet for most use cases as it’s only slightly slower than the other two, but offers much better protections against copying its data unsafely. It also implements the io.Writer interface which makes it amazingly flexible.

If you know better ways to concatenate strings in Go, please do share it in the comments section below. Thanks for reading, and happy coding!