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
162: sync.Pool for Object Reuse
I've seen this exact bug pop up in production more times than I'd like to admit. It usually happens when a team is trying to optimize a high-throughput service. They notice the garbage collector (GC) is working overtime, so they implement a sync.Pool to reuse objects and stop the constant allocation cycle. Everything looks great in the benchmarks, but then the logs start looking... weird.
Take a look at this snippet. It's a simple system designed to format request logs into a byte slice before sending them to a writer.
type LogFormatter struct {
pool sync.Pool
}
func NewLogFormatter() *LogFormatter {
return &LogFormatter{
pool: sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
}
func (lf *LogFormatter) Format(msg string) []byte {
buf := lf.pool.Get().(*bytes.Buffer)
buf.WriteString("LOG: ")
buf.WriteString(msg)
result := buf.Bytes()
lf.pool.Put(buf)
return result
}
The ghost data problem
At first glance, this looks efficient. We're reusing bytes.Buffer objects to avoid allocating a new one every time Format is called. But if you run this in a loop, you'll notice something terrifying: your logs start growing. The first call returns "LOG: Hello", the second returns "LOG: HelloLOG: World", and by the hundredth call, you have a massive string containing every log message since the program started.
The issue is that sync.Pool doesn't "reset" the object for you. It just gives you back a pointer to whatever was left in the pool. Since we never cleared the buffer, the next user of that buffer just keeps appending to the existing data. I call this "ghost data"—state leaking from one request into another. In a real-world scenario, this could be a catastrophic security leak if you're pooling buffers that hold user session data.
Resetting state before reuse
The fix is simple, but easy to forget: you must ensure the object is in a clean state. You can do this right after you Get() the object, or right before you Put() it back. I generally prefer doing it before the Put() so the object sits in the pool already cleaned and ready for the next consumer.
func (lf *LogFormatter) Format(msg string) []byte {
buf := lf.pool.Get().(*bytes.Buffer)
buf.WriteString("LOG: ")
buf.WriteString(msg)
// We must copy the bytes because buf.Bytes() returns a slice
// pointing to the buffer's internal memory. If we Put(buf)
// and then someone else modifies it, our 'result' changes!
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
buf.Reset() // Clear the buffer for the next user
lf.pool.Put(buf)
return result
}
Notice that I added a copy here as well. This is the second most common mistake with sync.Pool. If you return buf.Bytes() directly and then put the buffer back in the pool, you've created a race condition. Another goroutine could Get() that buffer and start writing to it while your original caller is still reading the result. Always be careful about who "owns" the memory after an object goes back into the pool.
When you actually need a pool
I want to give you a word of caution: don't reach for sync.Pool just because it sounds "fast." It adds complexity and, as we saw, creates opportunities for state-leak bugs.
You should use a pool when:
- You are allocating large objects frequently (like
[]bytebuffers or complex structs). - The GC pauses are becoming a bottleneck in your profiling (use
pprofto verify this). - The objects are expensive to create but cheap to reset.
Remember that sync.Pool is not a cache. The runtime can clear the pool at any time (usually during a GC cycle), so you can never rely on a specific object staying in the pool. It's purely a performance optimization to reduce allocation pressure.
📋 Practical Task
Implementing a Pooled JSON Encoder
You are building a high-performance metrics exporter. Currently, the app creates a new bytes.Buffer for every single metric exported, which is causing significant GC overhead. Your task is to implement a MetricEncoder that uses a sync.Pool to reuse buffers.
Requirements:
- Create a
MetricEncoderstruct with async.Pool. - Implement a
Encode(name string, value float64) []bytemethod. - The output format should be
name=value\n. - Ensure that buffers are properly reset so that metrics from previous calls do not appear in subsequent calls.
- Ensure that the returned
[]byteis a fresh copy of the data, so that reusing the buffer in the pool doesn't overwrite the result being used by the caller.
There are no comments for now.