← Recipes

阅读中文版本

Reduce Go Allocations with sync.Pool

Reuse hot short-lived buffers through sync.Pool to cut garbage collection pressure on high-throughput paths.

~30 minperformancegomemory

Task

Introduce a sync.Pool for a frequently allocated buffer to reduce allocation churn.

When to use this

A high-throughput handler allocates the same buffer per request, and GC pressure limits throughput.

Prerequisites

  • Go 1.21 or later
  • A benchmark for the hot path
  • Access to allocation profiling

Steps

1. Reuse buffers via a sync.Pool and reset on get.

var bufPool = sync.Pool{New: func() any { return make([]byte, 0, 32*1024) }}

func handle(data []byte) {
    buf := bufPool.Get().([]byte)[:0]
    defer bufPool.Put(buf)
    buf = append(buf, data...)
}

Expected output: Buffers are reused across requests instead of allocated per call.

2. Benchmark allocations before and after.

go test -bench=Handle -benchmem ./...

Expected output: Allocs per op drop, and GC pause time under load falls.

Troubleshooting

Problem

Data leaks between uses

Solution

Reset the slice length to zero on get and never rely on pooled contents.

Problem

Pool grows unbounded

Solution

Cap the buffer size and avoid pooling very large slices that belong on the heap.

Limitations

  • Pools do not help allocation-light paths.
  • Retained memory can increase RSS.
  • Correctness depends on resetting state.

What this recipe does

Introduce a sync.Pool for a frequently allocated buffer to reduce allocation churn.

A high-throughput handler allocates the same buffer per request, and GC pressure limits throughput.

Prerequisites

  • Go 1.21 or later
  • A benchmark for the hot path
  • Access to allocation profiling

Procedure

  1. Reuse buffers via a sync.Pool and reset on get.
var bufPool = sync.Pool{New: func() any { return make([]byte, 0, 32*1024) }}

func handle(data []byte) {
    buf := bufPool.Get().([]byte)[:0]
    defer bufPool.Put(buf)
    buf = append(buf, data...)
}

Expected result: Buffers are reused across requests instead of allocated per call.

  1. Benchmark allocations before and after.
go test -bench=Handle -benchmem ./...

Expected result: Allocs per op drop, and GC pause time under load falls.

How to confirm it worked

Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows benchmarks show fewer allocations per operation and GC pause time drops without behaviour changes.

Troubleshooting

Data leaks between uses — Reset the slice length to zero on get and never rely on pooled contents.

Pool grows unbounded — Cap the buffer size and avoid pooling very large slices that belong on the heap.

Limitations

  • Pools do not help allocation-light paths.
  • Retained memory can increase RSS.
  • Correctness depends on resetting state.