All Benchmarks

String Concatination — Go Benchmark

Comparing string concatenation approaches in Go — from the naive + operator to strings.Builder and beyond.

stringconcatinationappendbuilderbuffer

Compares four common ways to build a string incrementally in Go. Each implementation appends the same character repeatedly, letting the final string grow over time to expose performance differences. The write behavior measures the cost of appending, while read measures the cost of materializing the final string.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/string-concatination
Compare at
CPUs

32 CPUs

Performance Comparison (lower is better)
CPU:

Simple Append

Fastest (Read)Slowest (Write)

Uses the + operator to concatenate strings. Each append copies the entire string, making the total cost O(n²). Reading is free since the result is already a string.

Performance — Simple Append (lower is better)
CPU:
const (
	// setupCount is the number of writes during setup for read benchmarks.
	setupCount = 10_000
)

// BenchmarkSimpleAppend uses the + operator for string concatenation.
// Each append copies the entire string, making the total cost O(n²).
func BenchmarkSimpleAppend_write(b *testing.B) {
	var s string
	for i := 0; i < b.N; i++ {
		s += "a"
	}
	sink = s
}

func BenchmarkSimpleAppend_read(b *testing.B) {
	var s string
	for i := 0; i < setupCount; i++ {
		s += "a"
	}
	b.ResetTimer()

	// The result is already a string, so reading is essentially free.
	for i := 0; i < b.N; i++ {
		sink = s
	}
}
1 CPU

Read

119742.5×faster(11974151%)thanAppend To Slice And Join
2047.9×faster(204692%)thanBuffer
1.3×faster(27%)thanString Builder

Write

14.4×slower(1339%)thanAppend To Slice And Join
60×slower(5896%)thanBuffer
128.8×slower(12780%)thanString Builder
32 CPUs

Read

100351×faster(10034995%)thanAppend To Slice And Join
3915.5×faster(391448%)thanBuffer
1.1×faster(12%)thanString Builder

Write

39.2×slower(3822%)thanAppend To Slice And Join
73.6×slower(7264%)thanBuffer
112.9×slower(11193%)thanString Builder

String Builder

Fastest (Write)

Uses strings.Builder to accumulate strings. Write is amortized O(1). Reading calls builder.String(), which uses an unsafe conversion and does not allocate.

Performance — String Builder (lower is better)
CPU:
const (
	// setupCount is the number of writes during setup for read benchmarks.
	setupCount = 10_000
)

// BenchmarkStringBuilder uses strings.Builder which writes to an internal
// byte slice with amortized O(1) appends.
func BenchmarkStringBuilder_write(b *testing.B) {
	var s strings.Builder
	for i := 0; i < b.N; i++ {
		s.WriteString("a")
	}
	sink = s.String()
}

func BenchmarkStringBuilder_read(b *testing.B) {
	var s strings.Builder
	for i := 0; i < setupCount; i++ {
		s.WriteString("a")
	}
	b.ResetTimer()

	// String() uses an unsafe conversion — no allocation.
	for i := 0; i < b.N; i++ {
		sink = s.String()
	}
}
1 CPU

Read

94543×faster(9454200%)thanAppend To Slice And Join
1616.9×faster(161594%)thanBuffer
1.3×slower(27%)thanSimple Append

Write

9×faster(795%)thanAppend To Slice And Join
2.1×faster(115%)thanBuffer
128.8×faster(12780%)thanSimple Append
32 CPUs

Read

89808.2×faster(8980724%)thanAppend To Slice And Join
3504.1×faster(350313%)thanBuffer
1.1×slower(12%)thanSimple Append

Write

2.9×faster(188%)thanAppend To Slice And Join
1.5×faster(53%)thanBuffer
112.9×faster(11193%)thanSimple Append

Buffer

Uses bytes.Buffer to accumulate strings. Write is amortized O(1). Reading calls buf.String(), which copies the internal buffer into a new string (allocates).

Performance — Buffer (lower is better)
CPU:
const (
	// setupCount is the number of writes during setup for read benchmarks.
	setupCount = 10_000
)

// BenchmarkBuffer uses bytes.Buffer which writes to an internal byte slice
// with amortized O(1) appends, similar to strings.Builder.
func BenchmarkBuffer_write(b *testing.B) {
	var buf bytes.Buffer
	for i := 0; i < b.N; i++ {
		buf.WriteString("a")
	}
	sink = buf.String()
}

func BenchmarkBuffer_read(b *testing.B) {
	var buf bytes.Buffer
	for i := 0; i < setupCount; i++ {
		buf.WriteString("a")
	}
	b.ResetTimer()

	// String() copies the internal buffer into a new string (allocates).
	for i := 0; i < b.N; i++ {
		sink = buf.String()
	}
}
1 CPU

Read

58.5×faster(5747%)thanAppend To Slice And Join
2047.9×slower(204692%)thanSimple Append
1616.9×slower(161594%)thanString Builder

Write

4.2×faster(317%)thanAppend To Slice And Join
60×faster(5896%)thanSimple Append
2.1×slower(115%)thanString Builder
32 CPUs

Read

25.6×faster(2463%)thanAppend To Slice And Join
3915.5×slower(391448%)thanSimple Append
3504.1×slower(350313%)thanString Builder

Write

1.9×faster(88%)thanAppend To Slice And Join
73.6×faster(7264%)thanSimple Append
1.5×slower(53%)thanString Builder

Append To Slice And Join

Slowest (Read)

Appends strings to a slice with append, then calls strings.Join to produce the final result. Write is cheap (slice append), but read iterates and allocates the joined string.

Performance — Append To Slice And Join (lower is better)
CPU:
const (
	// setupCount is the number of writes during setup for read benchmarks.
	setupCount = 10_000
)

// BenchmarkAppendToSliceAndJoin collects strings in a slice and uses
// strings.Join to produce the final result.
func BenchmarkAppendToSliceAndJoin_write(b *testing.B) {
	var s []string
	for i := 0; i < b.N; i++ {
		s = append(s, "a")
	}
	sink = strings.Join(s, "")
}

func BenchmarkAppendToSliceAndJoin_read(b *testing.B) {
	var s []string
	for i := 0; i < setupCount; i++ {
		s = append(s, "a")
	}
	b.ResetTimer()

	// Join iterates through all elements and allocates the final string.
	for i := 0; i < b.N; i++ {
		sink = strings.Join(s, "")
	}
}
1 CPU

Read

58.5×slower(5747%)thanBuffer
119742.5×slower(11974151%)thanSimple Append
94543×slower(9454200%)thanString Builder

Write

4.2×slower(317%)thanBuffer
14.4×faster(1339%)thanSimple Append
9×slower(795%)thanString Builder
32 CPUs

Read

25.6×slower(2463%)thanBuffer
100351×slower(10034995%)thanSimple Append
89808.2×slower(8980724%)thanString Builder

Write

1.9×slower(88%)thanBuffer
39.2×faster(3822%)thanSimple Append
2.9×slower(188%)thanString Builder

Contributors