Mastering Go’s sync.Pool: Deep Dive into Object Pooling and Performance

This article explains how Go's sync.Pool works, shows practical usage examples, walks through its source code, and details the underlying implementation, including Put/Get methods, pinning, and garbage‑collection interaction, helping developers efficiently reuse objects in concurrent programs.

Go Programming World
Go Programming World
Go Programming World
Mastering Go’s sync.Pool: Deep Dive into Object Pooling and Performance

sync.Pool is a Go concurrency primitive used for object pooling; it caches temporary objects to reduce memory allocation and garbage‑collection pressure.

Overview

The article explores sync.Pool, providing usage examples and source‑code analysis to fully understand its design.

Structure

sync.Pool is a struct with the following exported fields:

type Pool struct {
    New func() any
    // Get retrieves an object; Put returns it to the pool.
}
New

: Called when the pool is empty to create a new object. Get: Retrieves an object from the pool, invoking New if necessary. Put: Returns an object to the pool for reuse.

Usage Example

The following example demonstrates a typical logger that reuses *bytes.Buffer objects via a sync.Pool.

package main

import (
    "bytes"
    "io"
    "os"
    "sync"
    "time"
)

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

func timeNow() time.Time { return time.Unix(1136214245, 0) }

func Log(w io.Writer, key, val string) {
    b := bufPool.Get().(*bytes.Buffer)
    b.Reset()
    b.WriteString(timeNow().UTC().Format(time.RFC3339))
    b.WriteByte(' ')
    b.WriteString(key)
    b.WriteByte('=')
    b.WriteString(val)
    w.Write(b.Bytes())
    bufPool.Put(b)
}

func main() { Log(os.Stdout, "path", "/search?q=flowers") }

Running the program prints:

$ go run main.go
2006-01-02T15:04:05Z path=/search?q=flowers

Typical Usage Pattern

Instantiate a sync.Pool and set the New function to create the cached object.

Call p.Get() to obtain an object; if the pool is empty, New is invoked.

After use, call p.Put(obj) to return the object to the pool.

sync.Pool is suitable for frequently created and destroyed objects, reducing allocation overhead and GC pressure, and should hold stateless objects.

Important Considerations

Objects retrieved from the pool may retain previous state; reset them before reuse.

Objects in the pool can be reclaimed by the GC, so they should not be relied upon to persist indefinitely.

Implementation Details

The Pool struct contains two core fields, local and victim, both pointing to poolLocal structures that store cached objects.

During a GC cycle, sync.Pool performs two actions:

Clears all objects in the victim cache.

Moves objects from local to victim, effectively turning the current cache into a “recycle bin”.

The victim behaves like a Windows Recycle Bin: objects remain there until a subsequent GC removes them.

Put Method

// Put adds an element to the pool
func (p *Pool) Put(x any) {
    if x == nil { return }
    l, _ := p.pin()
    if l.private == nil {
        l.private = x
    } else {
        l.shared.pushHead(x)
    }
    runtime_procUnpin()
}

The method pins the current goroutine to a processor (P), stores the object either in the private slot or the shared lock‑free queue, and then unpins.

Get Method

func (p *Pool) Get() any {
    l, pid := p.pin()
    x := l.private
    l.private = nil
    if x == nil {
        x, _ = l.shared.popHead()
        if x == nil {
            x = p.getSlow(pid)
        }
    }
    runtime_procUnpin()
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

The method first checks the private slot, then the local shared queue, and finally the slow path that steals from other Ps or the victim cache.

Pin Operation

func (p *Pool) pin() (*poolLocal, int) {
    if p == nil { panic("nil Pool") }
    pid := runtime_procPin()
    s := runtime_LoadAcquintptr(&p.localSize)
    l := p.local
    if uintptr(pid) < s {
        return indexLocal(l, pid), pid
    }
    return p.pinSlow()
}

Pin fixes the goroutine to a specific P, allowing lock‑free operations on that P's local cache. The slow path handles initialization and resizing.

Garbage‑Collection Cleanup

func poolCleanup() {
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }
    for _, p := range allPools {
        p.victim = p.local
        p.victimSize = p.localSize
        p.local = nil
        p.localSize = 0
    }
    oldPools, allPools = allPools, nil
}

Registered via runtime_registerPoolCleanup, this function runs at the start of each GC stop‑the‑world pause, moving primary caches to the victim cache and clearing the old victim caches.

Execution Flow

Diagrams (omitted here) illustrate the Put and Get processes, showing how objects travel between private slots, shared queues, local caches, and the victim cache across GC cycles.

Conclusion

sync.Pool provides an efficient way to reuse temporary objects in highly concurrent Go programs, reducing allocation overhead and GC pressure. Understanding its internal structures— local, victim, poolLocal, and the pin/unpin mechanism—helps developers use it correctly and avoid pitfalls such as stale state.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyGosync.PoolObject Pooling
Go Programming World
Written by

Go Programming World

Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.