🍒 Concatenate strings in Go

introduction strings

In Go, there are several methods for string concatenation. The simplest one (but not the best one) is to add two strings using the plus (+) operator. You can also use the fmt.Sprintf() function if you have a couple of strings that want to combine into one. A more efficient way to concatenate strings is to use the strings.Builder structure which minimizes memory copying and is recommended for building longer results. Alternatively, you can also use the strings.Join() function if you know all the strings to join and their separator before calling it, or bytes.Buffer and byte slices to operate directly on string bytes.


There is no single best method to concatenate strings, so it is a good idea to know most of them, their pros and cons, and use them depending on your use case.

All benchmarks in the examples below were run in Go 1.17.

Use the plus (+) operator to simply concatenate strings

Using the plus (+) operator is the simplest way of strings concatenation. However, you should be careful when calling this method because strings in Go are immutable, and every time you add a string to an existing variable, a new string is allocated in memory. As a result, this method is inefficient if you need to concatenate multiple strings, for example, in a loop. In that case, you should use the strings.Builder method to build the final string.

This and all the following examples give the output: Hello https://gosamples.dev 😉.

package main

import "fmt"

func main() {
    hello := "Hello"
    gosamples := "https://gosamples.dev"

    result := hello
    result += " "
    result += gosamples
    fmt.Println(result)
}

Pros and cons of concatenating strings using the plus (+) operator:

Pros
  • The easiest way of concatenating strings
  • No need to use external dependencies
Cons
  • Inefficient when used to concatenate a long list of strings
  • Less readable than fmt.Sprintf() method when building a formatted string

Use the fmt.Sprintf() function to format multiple strings into one

The fmt.Sprintf() function takes a template and arguments and returns this template with the appropriate fields replaced by the arguments. This is the most idiomatic and readable method for creating short strings with variable values but is not suitable for concatenation when you do not know the number of strings in advance.

package main

import "fmt"

func main() {
    hello := "Hello"
    gosamples := "https://gosamples.dev"

    result := fmt.Sprintf("%s %s", hello, gosamples)
    fmt.Println(result)
}

Pros and cons of concatenating strings using the fmt.Sprintf() function:

Pros
  • A clear and idiomatic way of creating strings with variable values
  • Allows to easily create strings from arguments of different types e.g. string, int, bool, etc., without explicit conversion
Cons
  • Not suitable when you do not know in advance the number of elements to concatenate
  • Inconvenient for a longer list of arguments

Use strings.Builder to efficiently concatenate strings

The strings.Builder was created to build long strings in a simple and efficient way. This method minimizes the number of copies and memory allocations and works particularly well if you have a large list of strings to concatenate or if the process of building the resulting string is multi-step. If you need to perform string concatenation efficiently, this method is the most recommended and natural choice in Go.

package main

import (
    "fmt"
    "log"
    "strings"
)

func main() {
    data := []string{"Hello", " ", "https://gosamples.dev"}

    var builder strings.Builder
    for _, s := range data {
        _, err := builder.WriteString(s)
        if err != nil {
            log.Fatal(err)
        }
    }

    fmt.Println(builder.String())
}

If you know the size of the output in advance, it is a good practice to use the Grow() method to preallocate the needed memory. This increases the speed of concatenation by avoiding unnecessary copying of partial results:

builder.Grow(27)

Pros and cons of concatenating strings using the strings.Builder:

Pros
  • Efficient for concatenating a long list of strings or for building a string in multiple steps
Cons
  • More complicated to use than the previous methods

Use the strings.Join() function to create a single string from a slice

The strings.Join() constructs a single string from joining a fixed slice of strings with a defined separator between them. It uses the strings.Builder internally. Since the number of elements to be concatenated is known, it allocates the necessary amount of memory, which makes this method very efficient.

package main

import (
    "fmt"
    "strings"
)

func main() {
    hello := "Hello"
    gosamples := "https://gosamples.dev"

    result := strings.Join([]string{hello, gosamples}, " ")
    fmt.Println(result)
}

Pros and cons of concatenating strings using the strings.Join() function:

Pros
  • Super-efficient for concatenating a fixed list of strings with the same separator
  • Simple and easy to use
Cons
  • Not suitable when you do not know in advance the number of elements to concatenate or if you want to use different separators

Use bytes.Buffer to efficiently build a byte buffer

The strings.Builder was introduced in Go 1.10. Before that, the bytes.Buffer was used to concatenate strings efficiently. It has similar methods but is slightly slower, so in the new code, you should use the strings.Builder instead.

package main

import (
    "bytes"
    "fmt"
    "log"
)

func main() {
    data := []string{"Hello", " ", "https://gosamples.dev"}

    var buffer bytes.Buffer
    for _, s := range data {
        _, err := buffer.WriteString(s)
        if err != nil {
            log.Fatal(err)
        }
    }

    fmt.Println(buffer.String())
}

As with the strings.Builder, you can use the Grow() method to preallocate the needed memory:

buffer.Grow(27)

Pros and cons of concatenating strings using the bytes.Buffer:

Pros
  • Efficient for concatenating a long list of strings or for building a string in multiple steps
Cons
  • Since Go 1.10, there is the strings.Builder which has similar methods and is more efficient

Use byte slice to extend a string

Strings in Go are read-only slices of bytes, so there is no problem extending a byte slice of a string by appending bytes of another string. As a result, after converting to a string, we get the concatenated output. However, this method is low-level and less idiomatic than the others. In practice, it is much better to use the strings.Builder instead.

package main

import (
    "fmt"
)

func main() {
    data := []string{"Hello", " ", "https://gosamples.dev"}

    var byteSlice []byte
    for _, s := range data {
        byteSlice = append(byteSlice, []byte(s)...)
    }

    fmt.Println(string(byteSlice))
}

Pros and cons of concatenating strings using appending to a byte slice:

Pros
  • Easy to use
  • Do not require any dependencies
Cons
  • Works on byte slices instead of strings - requires final conversion to string

  • Not as efficient as the bytes.Buffer

Benchmarks

To check which method of concatenating strings is the fastest, we prepared a simple benchmark that compares all the above methods. Each of them concatenates 399 elements into a single result. We simulated two variants of concatenation: when we know the size of the result string in advance (benchmarks named Benchmark<XXX>KnownSize), and when we do not know the exact size of the result string (benchmarks named Benchmark<XXX>UnknownSize). We did this because some methods are only suitable when we know the number of elements to concatenate (strings.Join(), fmt.Sprintf()), some work without considering the number of elements (the plus (+) operator), and some work in both variants (strings.Builder, bytes.Buffer, byte slice):

The fastest method does not mean the best. Always consider the readability of the code and try to fit the method to the use case.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package benchmark

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

const (
    hello     = "hello"
    gosamples = "https://gosamples.dev"
)

func generateStrings(withSeparator bool) (data []string, size int) {
    for i := 0; i < 100; i++ {
        data = append(data, hello)
        size += len(hello)
        if withSeparator {
            data = append(data, " ")
            size++
        }
        data = append(data, gosamples)
        size += len(gosamples)
        if withSeparator {
            data = append(data, " ")
            size++
        }
    }
    if withSeparator {
        data = data[:len(data)-1]
        size--
    }

    return data, size
}

func BenchmarkPlusOperatorUnknownSize(b *testing.B) {
    data, _ := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        for _, d := range data {
            s += d
        }
        _ = s
    }
}

func BenchmarkSprintfKnownSize(b *testing.B) {
    data, _ := generateStrings(false)
    var interfaceData []interface{}
    for _, d := range data {
        interfaceData = append(interfaceData, d)
    }
    format := strings.Repeat("%s ", len(interfaceData))
    format = strings.TrimSuffix(format, " ")
    var s string
    for n := 0; n < b.N; n++ {
        s = fmt.Sprintf(format, interfaceData...)
        _ = s
    }
}

func BenchmarkStringBuilderUnknownSize(b *testing.B) {
    data, _ := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        var builder strings.Builder
        for _, s := range data {
            builder.WriteString(s)
        }
        s = builder.String()
        _ = s
    }
}

func BenchmarkStringBuilderKnownSize(b *testing.B) {
    data, size := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        var builder strings.Builder
        builder.Grow(size)
        for _, s := range data {
            builder.WriteString(s)
        }
        s = builder.String()
        _ = s
    }
}

func BenchmarkJoinKnownSize(b *testing.B) {
    data, _ := generateStrings(false)
    var s string
    for n := 0; n < b.N; n++ {
        s = strings.Join(data, " ")
        _ = s
    }
}

func BenchmarkBytesBufferUnknownSize(b *testing.B) {
    data, _ := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        var buffer bytes.Buffer
        for _, s := range data {
            buffer.WriteString(s)
        }
        s = buffer.String()
        _ = s
    }
}

func BenchmarkBytesBufferKnownSize(b *testing.B) {
    data, size := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        var buffer bytes.Buffer
        buffer.Grow(size)
        for _, s := range data {
            buffer.WriteString(s)
        }
        s = buffer.String()
        _ = s
    }
}

func BenchmarkByteSliceUnknownSize(b *testing.B) {
    data, _ := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        var byteSlice []byte
        for _, s := range data {
            byteSlice = append(byteSlice, []byte(s)...)
        }
        s = string(byteSlice)
        _ = s
    }
}

func BenchmarkByteSliceKnownSize(b *testing.B) {
    data, size := generateStrings(true)
    var s string
    for n := 0; n < b.N; n++ {
        byteSlice := make([]byte, 0, size)
        for _, s := range data {
            byteSlice = append(byteSlice, []byte(s)...)
        }
        s = string(byteSlice)
        _ = s
    }
}

Running the benchmark with the command:

go test -bench=.

we got the results:

BenchmarkPlusOperatorUnknownSize-8           166          12008232 ns/op
BenchmarkSprintfKnownSize-8               184053              6421 ns/op
BenchmarkStringBuilderUnknownSize-8       269620              4365 ns/op
BenchmarkStringBuilderKnownSize-8         422790              2735 ns/op
BenchmarkJoinKnownSize-8                  475293              2370 ns/op
BenchmarkBytesBufferUnknownSize-8         219260              5441 ns/op
BenchmarkBytesBufferKnownSize-8           321973              3639 ns/op
BenchmarkByteSliceUnknownSize-8           175533              6803 ns/op
BenchmarkByteSliceKnownSize-8             230568              5046 ns/op

The benchmark is also available as a Github Gist.

🖨️ Convert string to []byte or []byte to string in Go

Learn the difference between a string and a byte slice
introduction strings slice

🔟 Convert string to bool in Go

Learn how to parse a string as a bool
introduction strings bool

👯 Remove duplicate spaces from a string in Go

Learn how to remove all redundant whitespaces from a string
introduction strings regex