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
You can reduce allocations further with Grow:
package main
import "strings"
func buildMessage(parts []string) string {
var b strings.Builder
b.Grow(100)
for _, part := range parts {
b.WriteString(part)
}
return b.String()
}
Grow(100) reserves space for about 100 bytes in advance.
Using bytes.Buffer
package main
import (
"bytes"
"fmt"
)
func main() {
var buf bytes.Buffer
buf.WriteString("Hello")
buf.WriteString(" ")
buf.WriteString("Go")
fmt.Println(buf.String())
}
This also works well, but if your goal is specifically to build a string, strings.Builder is usually the more direct choice.
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"Go"- builder now contains:
"Go"
-
Second iteration:
partis" is"b.WriteString(part)appends it- builder now contains:
"Go is"
-
Third iteration:
partis" fast"b.WriteString(part)appends it- builder now contains:
"Go is fast"
-
b.String()returns the final built string. -
fmt.Println(result)prints:
Go is fast
The important difference is that the builder is designed to grow efficiently as data is appended, instead of rebuilding a brand-new string on every loop iteration.
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.
var b strings.Builder
b.Grow(256)
4. Guard clauses before building
Developers often avoid unnecessary work:
func formatItems(items []string) string {
if len(items) == 0 {
return ""
}
var b strings.Builder
for _, item := range items {
b.WriteString(item)
}
return b.String()
}
5. bytes.Buffer when working with bytes and output streams
If code writes mixed binary/text data or needs an io.Writer, bytes.Buffer is very common.
func buildData() string {
var buf bytes.Buffer
buf.WriteString("status=")
buf.WriteString("ok")
return buf.String()
}
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.
Problematic idea:
var b strings.Builder
b.WriteString("hello")
b2 := b
b2.WriteString(" world")
Avoid copying builders. Pass pointers if needed, or keep one builder in one place.
Ignoring errors from methods that return them
With strings.Builder, WriteString returns (int, error), but the error is always nil. Beginners sometimes overcomplicate handling here.
_, _ = b.WriteString("hello")
In simple code, it is fine to ignore the return values.
Using bytes.Buffer when strings.Builder is clearer
bytes.Buffer is still useful, but for pure string building, strings.Builder better expresses intent.
Premature optimization for tiny concatenations
For a small fixed number of strings, simple concatenation is fine:
full := first + " " + last
Not every concatenation needs a builder.
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 bytes.Buffer
| Feature | strings.Builder | bytes.Buffer |
|---|---|---|
| Main purpose | Build strings | Build bytes/text buffers |
| Best for | String output | Byte-oriented output or io.Writer usage |
Has WriteString | Yes | Yes |
| Returns final string | String() | String() |
| Typical recommendation | Prefer for strings | Prefer for bytes or mixed writer use |
When simple + is okay
name := first + " " + last
This is perfectly fine when:
- there are only a few parts
- it is not inside a heavy loop
- readability matters more than micro-optimization
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
Rule of thumb
- Few strings: use
+ - Many appends in a loop: use
strings.Builder - Existing slice of strings: use
strings.Join - Byte-oriented buffering: use
bytes.Buffer
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 WriteString on strings.Builder return an error?
It returns (int, error), but the error is always nil. In most cases, the return values can be ignored safely.
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
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.