Compare the performance of string concatenation approaches: +, fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join.
Question 392MediumGo 1.22 to 1.25
Strings are immutable, so s += x in a loop allocates a new string and copies everything each iteration: O(n²) bytes copied and n allocations.
func concatPlus(parts []string) string {
var s string
for _, p := range parts {
s += p // n allocations, quadratic copying
}
return s
}
func concatBuilder(parts []string) string {
n := 0
for _, p := range parts {
n += len(p)
}
var sb strings.Builder
sb.Grow(n) // one allocation
for _, p := range parts {
sb.WriteString(p)
}
return sb.String() // no copy: returns the underlying bytes as a string
}
- a + b + c in a single expression is fine: the compiler emits one
concatstringscall with a single allocation. - fmt.Sprintf: slowest — reflection-based formatting and boxing arguments into
any. Fine for logs/errors, not for hot paths; usestrconv.AppendIntand friends instead. - strings.Builder: best for building strings;
String()is zero-copy. - bytes.Buffer: similar speed, but
String()copies; use it when you also needio.Readeror will read bytes back. - strings.Join: when you already have a slice — it pre-computes the length and allocates once.
Gotcha: Grow matters; without it Builder grows geometrically (amortized fine, but several allocations).
More on Performance, Profiling & Testing
- Q390What is bounds check elimination (BCE), how do you see it, and how can you help the compiler?
- Q391What are the tradeoffs of sync.Pool? When does it help, and when does it hurt?
- Q393What does this program do?
- Q394How do GOGC and GOMEMLIMIT interact, and how would you tune the GC for a latency-sensitive service?
- Q395Why does preallocating slices and maps matter? What does this print?
- Q396What is false sharing and how would you detect and fix it in Go?