String concatenation in Go
This article explores a few different ways to concatenate strings in Go, 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 trade-offs.
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))
}
Conclusion
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:
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!