Runtime and memory · 3 MIN READ

A pool reuses temporary objects; it does not own your results

Use sync.Pool with exclusive ownership, independent output and an explicit policy for oversized buffers.

Pooling can reduce temporary allocation, but every Get and Put creates an ownership obligation. A former owner must stop using the object after Put. A result that still aliases the object violates that obligation even when the code compiles and a small benchmark looks fast.

Temporary object reuse · A MAP OF THE IDEA

  1. Get temporary object
  2. One exclusive owner
  3. Copy external result
  4. Put ends ownership

Useful when

  • Reuse measured temporary allocation
  • Keep the boundary in one small function
  • Correct with reuse or fresh allocation

Watch out

  • No access after Put
  • No guaranteed object retention
  • No hard memory bound

Implementation & operation cost: Extra ownership protocol and possible retained capacity. Keep an unpooled baseline and discard oversized objects according to workload.

01

Draw the ownership cycle

Follow Get to one active owner, then produce an independent result before Put. After release, the old owner must not read or write the buffer. The pool can discard objects and Get need not return a previously stored object, so correctness must work with both reuse and a fresh allocation.

02

Copy the result before releasing its storage

This encoder returns owned bytes. Its example cutoff avoids deliberately returning an oversized backing buffer to this pool. The cutoff is a workload policy, not a universal value and not a cap on total process memory.

go
package main

import (
    "bytes"
    "fmt"
    "sync"
)

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

func encode(name string) []byte {
    buffer := buffers.Get().(*bytes.Buffer)
    buffer.Reset()
    buffer.WriteString("hello, ")
    buffer.WriteString(name)
    result := bytes.Clone(buffer.Bytes())
    if buffer.Cap() <= 64*1024 {
        buffer.Reset()
        buffers.Put(buffer)
    }
    return result
}

func main() {
    first := encode("Ada")
    _ = encode("Grace")
    fmt.Println(string(first))
}
Read the code

The output remains hello, Ada after another encode. bytes.Clone separates the result from temporary storage before Put. The example never assumes the second call gets the same buffer. Buffers larger than the illustrative cutoff are discarded from this reuse path, while active requests and returned results still consume memory.

03

Do not turn a pool into a resource budget

sync.Pool is not a bounded cache, a reliable queue, or a database connection manager. Objects can disappear without explicit removal. A strict concurrency limit needs admission control; a database connection needs its client library's capacity, timeout and cleanup rules. A pool alone establishes none of those contracts.

04

Test public independence, not object identity

Keep the first result, make many later calls, and verify the first bytes remain unchanged. Add parallel calls under the race detector. Do not assert that a specific object returns from Get. Test large and small inputs, and check cancellation ownership if work is queued to another goroutine.

05

Make the optimization earn its protocol

Compare a correct unpooled baseline against the pooled version using realistic response-size distributions. Measure allocations, retained memory and latency after bursts. Race instrumentation is for correctness checks, not production timing claims. If pooling adds complexity without a measurable benefit, keep the simpler owned-result implementation.

TAKE THIS WITH YOU

Get gives temporary exclusive access; Put ends it. Make external results independent before release, avoid identity assumptions and measure whether pooling helps. A per-object size cutoff does not bound total memory or concurrent work.

Check your understanding

A function returns a buffer to sync.Pool. How can its returned []byte remain safe for the caller?

Go to the source

Progress stays in this browser. No account needed.