Go

How does sync.Pool work, how does it interact with the GC, and what are the common misuse pitfalls?

Question 228HardGo 1.22 to 1.25

sync.Pool is a cache of temporary objects, sharded per P (processor), that reduces allocation pressure. Get tries, in order:

  1. the local P's private slot,
  2. the local P's shared queue,
  3. stealing from other Ps,
  4. the victim cache,
  5. calling New.

GC interaction: since Go 1.13, each GC moves the primary cache into a victim cache and throws away the old victim. An object survives at most about two GC cycles. A pool is not a connection pool or a free list, and you must never rely on an object still being there.

var bufPool = sync.Pool{
	New: func() any { return new(bytes.Buffer) },
}

func render(w io.Writer, v any) error {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset() // always reset: state from the previous user remains
	defer func() {
		if buf.Cap() <= 64<<10 { // don't keep huge buffers alive
			bufPool.Put(buf)
		}
	}()
	if err := json.NewEncoder(buf).Encode(v); err != nil {
		return err
	}
	_, err := w.Write(buf.Bytes())
	return err
}

Pitfalls:

  • Putting a slice ([]byte) by value allocates when it is converted to any (staticcheck SA6002). Store pointers instead.
  • Forgetting to reset leaks data between requests. This can be a security bug.
  • Pooling objects of very different sizes pins memory (Go issue 23199).
  • Using an object after Put is a use-after-free style bug.

Rule: only add a pool once a profile shows allocation is the bottleneck.

More on Concurrency Patterns & sync

All 38 Concurrency Patterns & sync questions