In Go, strings are immutable, meaning once created, their content cannot be modified. Therefore, each time you concatenate strings, a new string object is generated, which can have performance implications.

Common String Concatenation Methods

Using the + Operator

1s1 := "Hello, "
2s2 := "world!"
3result := s1 + s2

Performance Analysis: Each use of the + operator creates a new string, allocating new space equal to the sum of the spaces occupied by the strings to be concatenated. This method is suitable for scenarios with few concatenations but performs poorly with frequent concatenations.

Using fmt.Sprintf

1s1 := "Hello, "
2s2 := "world!"
3result := fmt.Sprintf("%s%s", s1, s2)

Performance Analysis: fmt.Sprintf uses reflection internally, resulting in lower performance. It is suitable for formatting strings but not recommended for frequent concatenations.

Using strings.Builder

1var builder strings.Builder
2builder.WriteString("Hello, ")
3builder.WriteString("world!")
4result := builder.String()

Performance Analysis: strings.Builder dynamically expands a byte slice, avoiding frequent memory allocations. It is suitable for scenarios with frequent concatenations and offers high performance.

Using bytes.Buffer

1var buffer bytes.Buffer
2buffer.WriteString("Hello, ")
3buffer.WriteString("world!")
4result := buffer.String()

Performance Analysis: bytes.Buffer is similar to strings.Builder, but strings.Builder is optimized for string concatenation and offers slightly better performance.

Using []byte and append

1var buf []byte
2buf = append(buf, "Hello, "...)
3buf = append(buf, "world!"...)
4result := string(buf)

Performance Analysis: Directly manipulating byte slices is flexible and efficient, making it suitable for scenarios with high performance requirements.

Performance Comparison

MethodSuitable Scenario
+ OperatorFew concatenations
fmt.SprintfFormatting strings
strings.BuilderFrequent concatenations, high performance required
bytes.BufferFrequent concatenations, relatively high performance required
[]byte + appendFrequent concatenations, extremely high performance required

Test Example

 1package main
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"strings"
 7	"testing"
 8)
 9
10// Using + operator
11func BenchmarkConcatOperator(b *testing.B) {
12	for i := 0; i < b.N; i++ {
13		s := ""
14		for range count {
15			s += testString
16		}
17	}
18}
19
20// Using fmt.Sprintf
21func BenchmarkSprintf(b *testing.B) {
22	for i := 0; i < b.N; i++ {
23		s := ""
24		for range count {
25			s = fmt.Sprintf("%s%s", s, testString)
26		}
27	}
28}
29
30// Using strings.Builder
31func BenchmarkStringsBuilder(b *testing.B) {
32	for i := 0; i < b.N; i++ {
33		var builder strings.Builder
34		builder.Grow(len(testString) * count) // Pre-allocate memory
35		for range count {
36			builder.WriteString(testString)
37		}
38		_ = builder.String()
39	}
40}
41
42// Using bytes.Buffer
43func BenchmarkBytesBuffer(b *testing.B) {
44	for i = 0; i < b.N; i++ {
45		var buffer bytes.Buffer
46		for range count {
47			buffer.WriteString(testString)
48		}
49		_ = buffer.String()
50	}
51}
52
53// Using []byte and append
54func BenchmarkByteSliceAppend(b *testing.B) {
55	for i = 0; i < b.N; i++ {
56		buf := make([]byte, 0, len(testString)*count) // Pre-allocate memory
57		for range count {
58			buf = append(buf, testString...)
59		}
60		_ = string(buf)
61	}
62}
 1$ go test -bench . -benchmem
 2goos: windows
 3goarch: amd64
 4pkg: demo    
 5cpu: 13th Gen Intel(R) Core(TM) i7-1360P
 6BenchmarkConcatOperator-16     177  7208800 ns/op 53164071 B/op 10000 allocs/op
 7BenchmarkSprintf-16            124  9689973 ns/op 53457215 B/op 20061 allocs/op
 8BenchmarkStringsBuilder-16  129318     8123 ns/op    10240 B/op     1 allocs/op
 9BenchmarkBytesBuffer-16      40520    29894 ns/op    42944 B/op    10 allocs/op
10BenchmarkByteSliceAppend-16 313455     3588 ns/op    10240 B/op     1 allocs/op
11PASS
12ok      demo    8.777s

Memory Allocation Mechanism of strings.Builder

The official recommendation is to use strings.Builder:

A Builder is used to efficiently build a string using Write methods. It minimizes memory copying.

Pre-allocation

If you need to manually control memory allocation, you can use the Grow method to pre-allocate sufficient memory, avoiding frequent reallocations:

The new capacity newCap is calculated as: newCap = 2*cap(b.buf) + n, using MakeNoZero to avoid zeroing operations.

1// grow copies the buffer to a new, larger buffer so that there are at least n
2// bytes of capacity beyond len(b.buf).
3func (b *Builder) grow(n int) {
4	buf := bytealg.MakeNoZero(2*cap(b.buf) + n)[:len(b.buf)] 
5	copy(buf, b.buf)
6	b.buf = buf
7}