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
Method | Suitable Scenario |
---|---|
+ Operator | Few concatenations |
fmt.Sprintf | Formatting strings |
strings.Builder | Frequent concatenations, high performance required |
bytes.Buffer | Frequent concatenations, relatively high performance required |
[]byte + append | Frequent 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}