How does sync.Pool behave, and what are its pitfalls?
Question 345HardGo 1.22 to 1.25
sync.Pool is a concurrency-safe cache of temporary, interchangeable objects. Each P has its own local shard, so Get and Put take no lock in the common case. Objects can be dropped at any time. Each GC moves the pool's contents into a victim cache, and whatever is left there is discarded at the next GC, so an idle pool empties after about two cycles.
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func render(w io.Writer, name string) error {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
defer func() {
if b.Cap() <= 64<<10 { // don't retain huge buffers
bufPool.Put(b)
}
}()
b.WriteString("hello, ")
b.WriteString(name)
_, err := w.Write(b.Bytes())
return err
}
Pitfalls:
- Put pointers, not values.
pool.Put(buf)with a[]byteboxes the slice header intoanyand allocates (staticcheck SA6002). Store*[]byteor a struct pointer. - Always reset state. Data left over from a previous user can leak between requests, which is a security bug.
- Do not use it for connections or anything else that needs lifecycle control. Items vanish without any close hook.
- Unbounded size variance: a single 50 MB request can leave a huge buffer in the pool. Cap what you Put back.
- Never use an object after you have Put it.
More on Memory, GC & Runtime Internals
- Q343Why is the process RSS much larger than the heap in use? How does Go return memory to the OS?
- Q344What techniques do you use to reduce allocations in a hot path?
- Q346How do you measure and assert allocation counts?
- Q347When does converting between
stringand[]byteNOT allocate? - Q348What does this print on a 64-bit platform, and why?
- Q349Explain 64-bit atomic alignment and false sharing. How do you lay out hot concurrent counters?