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.
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=flowersTypical 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Go Programming World
Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
