Problem Statement
Your JSON API service allocates and discards thousands of byte buffers per second, causing GC pauses. Implement object pooling using sync.Pool to reuse allocations.
What is sync.Pool?
A pool of temporary objects that can be saved and retrieved. Objects may be removed by GC at any time without notification.
Basic Usage
package main
import (
"bytes"
"sync"
)
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processRequest(data []byte) []byte {
// Get buffer from pool
buf := bufferPool.Get().(*bytes.Buffer)
// Ensure cleanup
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
// Use buffer
buf.Write(data)
buf.WriteString(" processed")
// Must copy result before returning buffer to pool
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result
}Benchmark: With vs Without Pool
func BenchmarkWithoutPool(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := new(bytes.Buffer)
buf.WriteString("hello world")
_ = buf.Bytes()
}
}
func BenchmarkWithPool(b *testing.B) {
pool := sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf := pool.Get().(*bytes.Buffer)
buf.WriteString("hello world")
_ = buf.Bytes()
buf.Reset()
pool.Put(buf)
}
}
// Results:
// BenchmarkWithoutPool-8 5000000 312 ns/op 112 B/op 2 allocs/op
// BenchmarkWithPool-8 20000000 78 ns/op 0 B/op 0 allocs/opJSON Encoder Pool (Real-World)
package jsonutil
import (
"bytes"
"encoding/json"
"sync"
)
var encoderPool = sync.Pool{
New: func() interface{} {
return &encoderBuffer{
buf: new(bytes.Buffer),
}
},
}
type encoderBuffer struct {
buf *bytes.Buffer
enc *json.Encoder
}
func Marshal(v interface{}) ([]byte, error) {
eb := encoderPool.Get().(*encoderBuffer)
defer func() {
eb.buf.Reset()
encoderPool.Put(eb)
}()
if eb.enc == nil {
eb.enc = json.NewEncoder(eb.buf)
}
if err := eb.enc.Encode(v); err != nil {
return nil, err
}
// Remove trailing newline from Encode
result := eb.buf.Bytes()
if len(result) > 0 && result[len(result)-1] == '\n' {
result = result[:len(result)-1]
}
// Copy before returning to pool
out := make([]byte, len(result))
copy(out, result)
return out, nil
}Pool Lifecycle with GC
func demonstrateGCInteraction() {
pool := sync.Pool{
New: func() interface{} {
fmt.Println("Creating new object")
return &bytes.Buffer{}
},
}
// Put objects in pool
for i := 0; i < 10; i++ {
pool.Put(&bytes.Buffer{})
}
fmt.Println("Before GC - getting from pool:")
_ = pool.Get() // Retrieves from pool
// Force GC - pool may be cleared
runtime.GC()
fmt.Println("After GC - getting from pool:")
_ = pool.Get() // May call New() if pool was cleared
}Common Mistakes
// ❌ WRONG: Using result after Put
buf := pool.Get().(*bytes.Buffer)
buf.WriteString("data")
result := buf.Bytes() // Points to pooled buffer
pool.Put(buf)
return result // DANGER: result may be corrupted
// ✅ CORRECT: Copy before Put
buf := pool.Get().(*bytes.Buffer)
buf.WriteString("data")
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
pool.Put(buf)
return resultFollow-up Questions
- Why doesn't sync.Pool have a Size() method?
- When would sync.Pool hurt performance?
- How does sync.Pool interact with GOMAXPROCS?