Question

Efficient String Concatenation in Go: strings.Builder, bytes.Buffer, and Best Practices

gostringstring-concatenation

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:

  1. Allocate memory for a new string
  2. Copy the old contents of s
  3. Copy the new part
  4. Replace s with 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.Builder for efficiently building strings
  • bytes.Buffer for 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 envelopes
  • strings.Builder = writing into one reusable growing buffer
Take Quiz

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

  • WriteString appends 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:

  1. parts is created with three strings:

    • "Go"
    • " is"
    • " fast"
  2. var b strings.Builder creates an empty builder.

  3. The loop starts.

  4. First iteration:

    • part is "Go"
    • b.WriteString(part) appends "Go"
    • builder now contains: "Go"
  5. Second iteration:

    • part is " is"
    • b.WriteString(part) appends it
    • builder now contains: "Go is"
  6. Third iteration:

    • part is " fast"
    • b.WriteString(part) appends it
    • builder now contains: "Go is fast"
  7. b.String() returns the final built string.

  8. 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.

Take Quiz

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

ApproachBest forProsCons
s += partSmall or infrequent concatenationSimple and readableInefficient in large loops
strings.BuilderBuilding strings incrementallyEfficient, clear intent, modern choiceShould not be copied after use
bytes.BufferBuilding bytes or writing through io.WriterFlexible, widely usedSlightly less direct for pure strings
strings.JoinJoining a slice of stringsSimple and efficientRequires parts to already be in a slice

strings.Builder vs bytes.Buffer

Featurestrings.Builderbytes.Buffer
Main purposeBuild stringsBuild bytes/text buffers
Best forString outputByte-oriented output or io.Writer usage
Has WriteStringYesYes
Returns final stringString()String()
Typical recommendationPrefer for stringsPrefer 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
Take Quiz

Cheat Sheet

Quick rules

  • Go strings are immutable.
  • Repeated += in loops can be inefficient.
  • Use strings.Builder for incremental string construction.
  • Use strings.Join when you already have []string.
  • Use bytes.Buffer when working with bytes or io.Writer patterns.
  • Use Grow(n) if you can estimate final size.
  • Do not copy a strings.Builder after 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.

Take Quiz

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

  1. Accept a slice of task names.
  2. Return a single formatted string containing one task per line.
  3. Prefix each task with its number.
  4. Use strings.Builder to build the result.
  5. Return an empty string if there are no tasks.
Take Quiz

Keep learning

Related questions

View more