Question
Efficient String Concatenation in Go: strings.Builder, bytes.Buffer, and Best Practices
Question
In Go, strings are immutable, so every change creates a new string value. If I need to concatenate many strings without knowing the final length in advance, what is the most efficient approach?
For example, this simple approach works, but it seems inefficient:
var s string
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
What should be used instead when building strings repeatedly in Go?
Short Answer
By the end of this page, you will understand why repeated += string concatenation is inefficient in Go, and how to build strings efficiently using strings.Builder and, in some cases, bytes.Buffer. You will also learn when simple concatenation is fine, how memory allocation affects performance, and what patterns are commonly used in real Go code.
Concept
Go strings are immutable, which means their contents cannot be changed after creation. When you write code like this:
s += part
Go cannot modify the existing string in place. Instead, it must:
- Allocate memory for a new string
- Copy the old contents of
s - Copy the new
part - Replace
swith the newly created string
If this happens inside a loop many times, the program repeatedly allocates and copies data. That can become slow and memory-heavy.
To solve this, Go provides types designed for incremental building:
strings.Builderfor efficiently building stringsbytes.Bufferfor efficiently building byte data, and sometimes strings
For string-heavy code, strings.Builder is usually the best modern choice. It avoids unnecessary intermediate string creation and is specifically intended for repeated string concatenation.
This matters in real programs because string building happens everywhere:
- generating HTML
- creating SQL queries
- formatting logs
- producing API responses
- assembling file content
Using the right tool improves both performance and code clarity.
Mental Model
Think of a Go string like a sealed envelope.
- Once created, you cannot open it and add more text inside.
- If you want a longer message, you must create a new envelope containing the old message plus the new text.
Now imagine doing that 1,000 times. Each time, you copy everything again.
A strings.Builder is like writing into a notebook instead of sealed envelopes.
- You keep adding text to the same growing workspace.
- At the end, you turn the notebook contents into a final string.
So:
+=in a loop = repeatedly creating new envelopesstrings.Builder= writing into one reusable growing buffer
Syntax and Examples
The most common efficient approach in Go is strings.Builder.
Basic syntax
var b strings.Builder
b.WriteString("Hello")
b.WriteString(", ")
b.WriteString("world")
result := b.String()
Example: replacing repeated +=
package main
import (
"fmt"
"strings"
)
func main() {
var b strings.Builder
for i := 0; i < 5; i++ {
b.WriteString("Go ")
}
result := b.String()
fmt.Println(result)
}
Output:
Go Go Go Go Go
Why this is better
WriteStringappends to the builder efficiently- fewer allocations are needed than repeated
s += ... - the code clearly communicates that you are constructing a string incrementally
If you know an approximate final size
Step by Step Execution
Consider this example:
package main
import (
"fmt"
"strings"
)
func main() {
parts := []string{"Go", " is", " fast"}
var b strings.Builder
for _, part := range parts {
b.WriteString(part)
}
result := b.String()
fmt.Println(result)
}
Here is what happens step by step:
-
partsis created with three strings:"Go"" is"" fast"
-
var b strings.Buildercreates an empty builder. -
The loop starts.
-
First iteration:
partis"Go"b.WriteString(part)appends
Real World Use Cases
Efficient string concatenation appears in many practical Go programs.
Building HTTP responses
A server may generate text output such as CSV, plain text, or small HTML fragments:
var b strings.Builder
b.WriteString("Name,Age\n")
for _, user := range users {
b.WriteString(user.Name)
b.WriteString(",")
b.WriteString(strconv.Itoa(user.Age))
b.WriteString("\n")
}
return b.String()
Creating log messages
When generating structured text logs, repeated appending may happen in loops.
Generating SQL strings carefully
If a query is assembled from known safe pieces, builders can help construct it efficiently.
Producing file content
You might generate config files, reports, or templates before writing them to disk.
Joining many small tokens
Parsers, code generators, and export tools often combine many small string fragments into one larger result.
Real Codebase Usage
In real Go codebases, developers commonly use a few patterns around string building.
1. strings.Builder for repeated appends
This is the usual choice when the final output is a string.
func buildPath(parts []string) string {
var b strings.Builder
for i, p := range parts {
if i > 0 {
b.WriteString("/")
}
b.WriteString(p)
}
return b.String()
}
2. strings.Join when you already have all parts
If the pieces are already stored in a slice, strings.Join is often simpler and very efficient.
func buildSentence(words []string) string {
return strings.Join(words, " ")
}
3. Grow when size is predictable
If you can estimate the final size, reserving capacity can reduce reallocations.
Common Mistakes
Using += inside large loops
This works, but can be inefficient for many concatenations.
var s string
for i := 0; i < 1000; i++ {
s += "x"
}
Prefer:
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("x")
}
s := b.String()
Forgetting that strings.Join may be simpler
If you already have a slice of strings, do not manually rebuild it.
Broken style:
var b strings.Builder
for i, word := range words {
if i > 0 {
b.WriteString(",")
}
b.WriteString(word)
}
Simpler:
s := strings.Join(words, ",")
Copying a strings.Builder
A builder should not be copied after it has been used.
Comparisons
| Approach | Best for | Pros | Cons |
|---|---|---|---|
s += part | Small or infrequent concatenation | Simple and readable | Inefficient in large loops |
strings.Builder | Building strings incrementally | Efficient, clear intent, modern choice | Should not be copied after use |
bytes.Buffer | Building bytes or writing through io.Writer | Flexible, widely used | Slightly less direct for pure strings |
strings.Join | Joining a slice of strings | Simple and efficient | Requires parts to already be in a slice |
strings.Builder vs
Cheat Sheet
Quick rules
- Go strings are immutable.
- Repeated
+=in loops can be inefficient. - Use
strings.Builderfor incremental string construction. - Use
strings.Joinwhen you already have[]string. - Use
bytes.Bufferwhen working with bytes orio.Writerpatterns. - Use
Grow(n)if you can estimate final size. - Do not copy a
strings.Builderafter writing to it.
Common syntax
var b strings.Builder
b.WriteString("hello")
b.WriteString(" world")
result := b.String()
Preallocate capacity
var b strings.Builder
b.Grow(128)
Join existing strings
result := strings.Join(parts, ",")
Fine for small fixed cases
result := a + b + c
FAQ
When should I use strings.Builder in Go?
Use it when you need to append many strings, especially inside a loop.
Is s += part always bad in Go?
No. It is fine for small, simple cases. It becomes inefficient when repeated many times.
Should I use bytes.Buffer or strings.Builder?
Use strings.Builder for string construction. Use bytes.Buffer when working with bytes or writer-based APIs.
Is strings.Join faster than a loop with WriteString?
If you already have all parts in a []string, strings.Join is often the simplest and very efficient choice.
Can I preallocate memory for a strings.Builder?
Yes. Use b.Grow(n) when you can estimate the final size.
Can I copy a strings.Builder value?
You should avoid copying it after it has been used. Keep one builder instance and use it directly.
Does on return an error?
Mini Project
Description
Build a small Go function that creates a text report from a list of tasks. This project demonstrates efficient string construction using strings.Builder instead of repeated += operations. It is practical because many real programs generate reports, logs, CSV data, or text responses in this way.
Goal
Create a function that takes a list of task names and returns a numbered text report as a single string.
Requirements
- Accept a slice of task names.
- Return a single formatted string containing one task per line.
- Prefix each task with its number.
- Use
strings.Builderto build the result. - Return an empty string if there are no tasks.
Keep learning
Related questions
Check if a Value Exists in a Slice in Go
Learn how to check whether a value exists in a slice in Go, and why Go has no Python-style `in` operator for arrays or slices.
Concatenating Slices in Go with append
Learn how to concatenate two slices in Go using append and the ... operator, with examples, pitfalls, and practical usage.
Convert String to Integer in Go: Idiomatic Parsing with strconv.Atoi
Learn the idiomatic way to convert a string to an int in Go using strconv.Atoi, with examples, errors, and common mistakes.