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