Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
112: strings.Builder for Efficient Concatenation
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.Builderfor 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"]
There are no comments for now.