The Gentle Art of Garbage Collection in Go
In the realm of systems programming, few topics generate as much discussion as memory management. Go’s garbage collector represents a fascinating case study in the art of doing less to achieve more—a principle that resonates deeply with the Taoist concept of Wu Wei (无为).
The Philosophy of Non-Action
Wu Wei doesn’t mean doing nothing. Rather, it suggests acting in harmony with the natural flow of things, without forcing or struggling against the current. Go’s GC embodies this principle through its concurrent, tri-color mark-and-sweep algorithm.
// The GC works silently in the background
// like water finding its own level
func processStream(ch <-chan DataPoint) {
for data := range ch {
// Allocate freely, trust the system
result := transform(data)
emit(result)
// No manual cleanup needed
}
}
Tuning Without Forcing
The key insight is that aggressive optimization often backfires. Instead of fighting the GC, we work with it:
1. Reduce Allocation Pressure
Rather than eliminating allocations entirely (which would be forcing), we reduce unnecessary ones:
// Before: Creates new slice each iteration
for i := 0; i < n; i++ {
data := make([]byte, 1024)
process(data)
}
// After: Reuse when natural
buffer := make([]byte, 1024)
for i := 0; i < n; i++ {
process(buffer)
}
2. Let the Scheduler Breathe
Go’s runtime scheduler and GC work together. Blocking operations create natural GC opportunities:
func worker(jobs <-chan Job) {
for job := range jobs {
result := compute(job)
// This I/O pause is a gift to the GC
sendResult(result)
}
}
The GOGC Dial
The GOGC environment variable controls the GC’s aggressiveness. The default of 100 means the GC triggers when heap size doubles. But remember:
“The Tao that can be told is not the eternal Tao.”
Similarly, the optimal GOGC value cannot be prescribed—it emerges from your specific workload.
# More aggressive GC (lower latency, more CPU)
GOGC=50 ./myservice
# Less aggressive GC (higher throughput, more memory)
GOGC=200 ./myservice
Measuring Without Obsessing
Use runtime.ReadMemStats() to observe, not to control:
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
log.Printf("HeapAlloc: %d MB", stats.HeapAlloc/1024/1024)
log.Printf("NumGC: %d", stats.NumGC)
log.Printf("PauseTotalNs: %d ms", stats.PauseTotalNs/1e6)
Conclusion
The most performant Go programs I’ve written weren’t the ones where I fought hardest against the GC. They were the ones where I understood its nature and designed my data flow to complement it.
Like water, the GC seeks the path of least resistance. Our job is not to dam it, but to shape the landscape through which it flows.
“The highest good is like water. Water benefits all things and does not compete.” — Tao Te Ching, Chapter 8
"Code is poetry written for machines, but read by humans. Optimize for the latter."