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
218: Reducing Garbage Collector Pressure
Go's garbage collector is fantastic—it's the reason we don't spend half our lives hunting for memory leaks like we did in C++. But "automatic" doesn't mean "free." When you're building high-throughput systems, the GC can become your primary bottleneck. Not because it's slow, but because you're giving it too much work to do. This is what we call GC pressure.
The goal isn't to avoid the heap entirely, but to stop the constant cycle of allocate-use-discard. I've found that the most common culprit is the temporary buffer—those little []byte or []int slices we create inside a loop or a request handler that live for a millisecond and then vanish.
The Cost of Tiny Allocations
Let's look at a common scenario: a telemetry processor. Imagine we're receiving thousands of raw byte packets per second and we need to format them into a standardized string for a database. Here is how I'd initially write this without thinking about the GC:
type Event struct {
ID int
Value float64
}
func formatEvent(e Event) string {
// Every time this is called, we allocate a new byte slice
buf := make([]byte, 0, 64)
buf = append(buf, []byte("ID:")...)
buf = append(buf, []byte(strconv.Itoa(e.ID))...)
buf = append(buf, []byte(" Val:")...)
buf = append(buf, []byte(fmt.Sprintf("%.2f", e.Value))...)
return string(buf)
}
On the surface, this is clean. But if you're processing 100,000 events per second, you're creating 100,000 short-lived slices. The GC has to track every single one of those, mark them as unreachable, and sweep them. You'll see your CPU usage spike, not because of your business logic, but because the runtime is frantically cleaning up after you.
The Dangerous Shortcut
When I first encountered this in a production app, my first instinct was to just move the buffer outside the function. I thought, "Why allocate every time? I'll just keep one buffer and reuse it."
var globalBuf = make([]byte, 0, 64)
func formatEventFast(e Event) string {
globalBuf = globalBuf[:0] // Reset length, keep capacity
globalBuf = append(globalBuf, []byte("ID:")...)
// ... append the rest ...
return string(globalBuf)
}
I pushed this to a staging environment and it crashed almost immediately. Why? Because I forgot that Go services are concurrent. Two goroutines tried to write to globalBuf at the same time, leading to a classic data race and corrupted strings. Never use a global slice for temporary work unless you have a very good reason and a very strong Mutex (which would then kill your performance anyway).
Implementing a Recyclable Buffer
The professional way to handle this is via sync.Pool. It's essentially a cache of objects that the GC can reclaim if the system is under memory pressure, but which you can reuse to avoid new allocations.
Here is how I'd actually implement the telemetry formatter to keep the GC happy:
var bufPool = sync.Pool{
New: func() any {
// We allocate a slice with a reasonable capacity
b := make([]byte, 0, 64)
return &b
},
}
func formatEventOptimized(e Event) string {
// Grab a buffer from the pool
ptr := bufPool.Get().(*[]byte)
buf := *ptr
// Reset the slice but keep the underlying array
buf = buf[:0]
buf = append(buf, "ID:"...)
buf = append(buf, strconv.Itoa(e.ID)...)
buf = append(buf, " Val:"...)
buf = append(buf, fmt.Sprintf("%.2f", e.Value)...)
res := string(buf)
// Put the buffer back for the next goroutine to use
*ptr = buf
bufPool.Put(ptr)
return res
}
Notice a few key things here. First, I'm storing a pointer to the slice (*[]byte) in the pool. This is because any (interface{}) takes up space, and passing the slice header by value can sometimes cause an accidental allocation itself. Second, I reset the length to zero using buf[:0], which keeps the capacity intact. Now, instead of 100,000 allocations per second, we might only have a few dozen—one for each active goroutine.
One final tip: when you're debugging this, run your tests with go test -bench . -benchmem. If you see "allocs/op" dropping toward zero, you know you've won.
📋 Practical Task
Exercise: Optimizing the Log Header Generator
You have a high-frequency logging function that attaches a metadata header to every log message. Currently, it allocates a new byte slice for every single call, causing significant GC pauses in a high-traffic environment.
Your Task: Refactor the generateHeader function below. Implement a sync.Pool to reuse the byte slices, ensuring that the implementation is thread-safe and minimizes heap allocations per call.
package main
import (
"fmt"
"sync"
"time"
)
// Current inefficient implementation
func generateHeader(requestID string, userID int) string {
buf := make([]byte, 0, 128)
buf = append(buf, "[Req:"...)
buf = append(buf, requestID...)
buf = append(buf, "] [User:"...)
buf = append(buf, []byte(fmt.Sprintf("%d", userID))...)
buf = append(buf, "] ")
return string(buf)
}
func main() {
// Test the function
fmt.Println(generateHeader("abc-123", 42))
}
Requirements:
- Create a
sync.Poolthat allocates a[]bytewith a capacity of 128. - Modify
generateHeaderto retrieve a buffer from the pool, reset it, and return it to the pool after the string is created. - Ensure you are handling the pool types correctly (using type assertion).
There are no comments for now.