sync.Pool is useful for short-lived allocations you make in high volume, like bytes.Buffer for building JSON responses. It is also one of the easiest things in Go to misuse: a common bug is keeping a pooled buffer that grew to 10 MB during one request, putting it back, and now every request carries that extra capacity.

Here is the pattern I have settled on. Guard the capacity on Put. Reset on Get only if needed.

const maxPooledCap = 64 * 1024 // 64 KB

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

func GetBuf() *bytes.Buffer {
	b := bufPool.Get().(*bytes.Buffer)
	b.Reset()
	return b
}

func PutBuf(b *bytes.Buffer) {
	if b.Cap() > maxPooledCap {
		return // let GC reclaim it
	}
	bufPool.Put(b)
}

// Usage:
func renderJSON(w io.Writer, v any) error {
	buf := GetBuf()
	defer PutBuf(buf)
	if err := json.NewEncoder(buf).Encode(v); err != nil {
		return err
	}
	_, err := w.Write(buf.Bytes())
	return err
}

Two gotchas:

  1. Do not return a slice of buf.Bytes() to a caller that outlives the defer PutBuf. The byte slice aliases the pooled buffer and will race.
  2. sync.Pool is not a memory pool. The GC will empty it on every cycle. Do not rely on items sticking around.

See also /posts/sync-pool-is-not-a-memory-pool/.