Skip to Content
Course content

112: strings.Builder for Efficient Concatenation

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this pattern countless times during code reviews. A developer is building a report or aggregating logs, and they use the most intuitive tool they know: the + operator. It looks clean, it's easy to read, and for a few iterations, it works perfectly. But once that code hits production with real-world data volumes, the application slows to a crawl or the memory usage spikes unpredictably.

package main

import "fmt"

func formatUserReport(users []string) string {
    var report string
    for _, user := range users {
        // This looks innocent, but it's a performance trap
        report += "User: " + user + "\n"
    }
    return report
}

func main() {
    users := []string{"Alice", "Bob", "Charlie", "Diana"} 
    fmt.Println(formatUserReport(users))
}

The Hidden Cost of the Plus Operator

The code above isn't "broken" in the sense that it produces the wrong output. If you run it with four users, it's lightning fast. But here is the problem: in Go, strings are immutable. They cannot be changed once they are created.

Every time you use +=, Go isn't just appending text to the end of your existing string. Instead, it's allocating a brand new block of memory, copying the entire contents of the old string into it, and then adding the new piece. If you have 1,000 users, you aren't doing 1,000 operations; you're copying more and more data with every single loop. It's an $O(n^2)$ operation that puts an immense amount of pressure on the Garbage Collector (GC) because you're leaving a trail of thousands of discarded, temporary strings in memory.

Switching to strings.Builder

To fix this, we use the strings.Builder type. Think of it as a specialized buffer that collects your string fragments in a mutable slice of bytes and only converts them into a final, immutable string at the very end. This avoids all those intermediate allocations.

package main

import (
    "fmt"
    "strings"
)

func formatUserReport(users []string) string {
    var builder strings.Builder

    for _, user := range users {
        builder.WriteString("User: ")
        builder.WriteString(user)
        builder.WriteByte('\n')
    }

    return builder.String()
}

func main() {
    users := []string{"Alice", "Bob", "Charlie", "Diana"}
    fmt.Println(formatUserReport(users))
}

I prefer builder.WriteString and builder.WriteByte because they are explicit and efficient. By using the builder, we've turned a memory-heavy process into a linear operation. The builder manages an internal buffer that grows exponentially, meaning it only needs to reallocate memory a handful of times, regardless of whether you have 10 users or 10,000.

Pre-allocating Memory with Grow

If you want to be a real pro about this, there's one more trick. If you have a rough idea of how large your final string will be, you can use the Grow() method. This tells the builder to allocate the necessary memory upfront, reducing the number of internal re-allocations to exactly one.

I usually do this when I know the average length of my data. For example, if I know I have 100 users and each line is roughly 20 characters, I'll call builder.Grow(2000) before the loop starts. It's a small addition, but in high-throughput systems, it's the difference between a service that stutters and one that glides.




📋 Practical Task

Building a Custom CSV-to-JSON Array Converter

You've been handed a slice of strings representing CSV rows (where each string is a comma-separated line of data). Your task is to convert these rows into a single, large JSON-formatted string array. Because the input could contain thousands of rows, you must use strings.Builder to ensure the conversion is memory-efficient.

Requirements:

  • Create a function ConvertCSVToJSON(rows []string) string.
  • The final string should start with [ and end with ].
  • Each row should be wrapped in double quotes and separated by a comma.
  • Use strings.Builder for all concatenation.
  • Bonus: Use builder.Grow() to pre-allocate memory based on an estimated average row length of 30 characters.
// Expected Input:
// rows := []string{"Apple,Red", "Banana,Yellow", "Grape,Purple"}

// Expected Output:
// ["Apple,Red","Banana,Yellow","Grape,Purple"]
Rating
0 0

There are no comments for now.

to be the first to leave a comment.