A sync.Pool of bytes.Buffer done right
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:
- Do not return a slice of
buf.Bytes()to a caller that outlives thedefer PutBuf. The byte slice aliases the pooled buffer and will race. sync.Poolis 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/.