Skip to content

Day 034 — strings.Builder & Efficient Concatenation

Month 2 · Week 1 · ⬅ Day 033 · Day 035 ➡ · Journal index

🎯 Learning Objective

Understand why += in a loop is quadratic, and use strings.Builder (and friends) to build strings with minimal allocations.

📚 Topics

  • String immutability → why += reallocates
  • strings.Builder: WriteString/WriteByte/WriteRune/Grow/String
  • strings.Join, bytes.Buffer, and when to pick which
  • The "don't copy a Builder" rule

📖 Reading / Sources

📝 Notes

  • Strings are immutable. s += x can't grow s in place — it allocates a new string and copies both operands. In a loop of n appends that's O(n²) total copying → [[string-immutability]].
  • strings.Builder writes into a single internal []byte that grows geometrically (amortized O(n)), and its String() returns the buffer without copying (it uses an unsafe conversion internally) → [[strings-builder]].
  • API: WriteString, WriteByte, WriteRune, and Write([]byte). Builder satisfies io.Writer, so fmt.Fprintf(&b, ...) works too.
  • b.Grow(n) pre-reserves capacity when you can estimate the final size, eliminating intermediate regrowth.
  • Do not copy a strings.Builder after first use — it holds a pointer to detect misuse and will panic. Pass it by pointer (*strings.Builder), never by value → [[builder-no-copy]].
  • For a known set of pieces, strings.Join(parts, sep) is simplest and already optimal — reach for Builder when you're appending in a loop with logic between writes.
  • bytes.Buffer also builds efficiently and is read+write; prefer strings.Builder when you only need to produce a string (it skips the final copy), and bytes.Buffer when you also need to read the bytes back or hand off an io.Reader.
  • Rule of thumb table:
  • Few fixed pieces → a + b + c or strings.Join.
  • Loop building a string → strings.Builder.
  • Need both read and write of bytes → bytes.Buffer.

💻 Code Examples

var b strings.Builder
b.Grow(16) // optional: preallocate if size is roughly known
for n := 0; n < 5; n++ {
    fmt.Fprintf(&b, "%d,", n) // &b — never copy a Builder by value
}
out := strings.TrimRight(b.String(), ",") // "0,1,2,3,4"
_ = out

Full code: examples/month-02/strings-strconv/main.go · Run: go run ./examples/month-02/strings-strconv

🏋️ Exercises / Practice

Exercise Status Link
Slugify text with strings.Builder (no += in loop) exercises/month-02/week-1/slugify

🐛 Mistakes Made

  • Built a CSV line with s += field + "," in a hot loop; switching to strings.Builder cut allocations dramatically.
  • Passed a strings.Builder by value to a helper → panic: strings: illegal use of non-zero Builder copied by value. Switched to *strings.Builder.

❓ Open Questions

  • Roughly how big does the loop need to be before Builder beats naive += in practice (with -benchmem)?

🧠 Active Recall (answer without looking)

  1. Q: Why is s += x inside a loop a performance trap?

    A Strings are immutable, so each `+=` allocates a brand-new string and copies all existing bytes — O(n²) work over `n` iterations. `strings.Builder` appends into one growing buffer for amortized O(n).

  2. Q: What happens if you pass a used strings.Builder by value?

    A It panics: `illegal use of non-zero Builder copied by value`. Builder tracks a self-pointer to forbid copies; always pass `*strings.Builder`.

🪶 Feynman Reflection

Because a Go string can never be edited, "adding to" one really means "make a whole new bigger one and throw the old away." Do that in a loop and you re-copy everything every time — death by a thousand copies. strings.Builder keeps one resizable scratchpad and only hands you a finished string at the end, so you copy the data essentially once.

🕳️ Knowledge Gaps

  • Writing a Go benchmark (testing.B, b.ReportAllocs) to prove the difference — coming up in the testing week.

✅ Summary

I know why naive concatenation is quadratic and can build strings efficiently with strings.Builder, strings.Join, or bytes.Buffer, choosing the right tool per situation.

⏭️ Next Steps / Prep for Tomorrow

  • Day 035: Week 1 review and closed-book recall.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦🟦⬜

Suggested commit: feat(exercises): slugify with strings.Builder (day 034)