Documentation
¶
Overview ¶
Package pool provides typed reuse of temporary values.
Purpose ¶
Package pool exists for code that repeatedly allocates short-lived mutable objects and would benefit from reusing them through a small, explicit, and type-safe API.
The package is intentionally narrow. It does not try to become a general object lifecycle framework, an allocator replacement, or a rich borrow/return subsystem. Instead, it focuses on one well-bounded pattern:
- acquire a temporary value of type T;
- use that value within one logical operation;
- return the value for possible reuse;
- optionally reject oversized or non-reusable values on the return path.
In code, the intended usage surface is deliberately small:
v := p.Get() // use v only within the current operation p.Put(v)
This package is therefore best understood as a typed, policy-driven runtime layered over sync.Pool-style temporary reuse rather than as a comprehensive resource manager.
Design model ¶
The package is built around three cooperating concepts:
- Options, which describes lifecycle policy declaratively;
- Pool, which exposes the public Get/Put runtime API;
- an internal backend, currently implemented on top of sync.Pool, which provides low-level storage and retrieval.
These layers intentionally separate concerns:
- Options answers "how should values be created, reset, retained, or observed when dropped?".
- Pool answers "how does the caller interact with the runtime?".
- the backend answers "where are already-clean reusable values stored?".
The package also contains an internal lifecycle controller that owns the canonical return-path order. That order is stable and central to the package contract:
- evaluate the reuse admission policy;
- if reuse is denied, invoke the drop callback and stop;
- if reuse is allowed, reset the value into a clean reusable state;
- store the cleaned value in the backend for future acquisition.
This order is deliberate. Reuse admission happens before reset so that a policy may inspect the object in the state it actually accumulated during use. Reset happens before backend storage so that retained values remain clean while idle and callers of Get observe values that are already ready for immediate use.
Why callbacks instead of a mandatory interface on T ¶
The package does not require T to implement Reset, Reusable, or any other package-specific interface. Lifecycle policy is instead expressed through callbacks in Options.
This choice is intentional for several reasons:
- domain types remain decoupled from the pool package;
- the same concrete type can be pooled under different reuse policies;
- reset and reuse logic stay explicit at the call site that assembles the pool;
- callers are not forced to encode pooling semantics into their domain model merely to participate in reuse.
For example, the same Frame type might be pooled under a small-capacity fast path and a larger-capacity batch path simply by constructing two pools with different ReuseFunc values.
Intended use ¶
Pool is intended for temporary values whose useful lifetime is limited to a single logical operation and whose repeated allocation would otherwise create avoidable GC pressure.
Typical good fits include:
- parser or decoder state objects;
- request-scoped scratch structures;
- reusable builders or envelopes;
- mutable helper structs used on hot paths;
- temporary work objects that can be restored to a well-defined clean state before reuse.
Typical poor fits include:
- long-lived domain entities whose ownership escapes the current operation;
- values that must remain reachable after Put returns;
- objects requiring stable inventory guarantees;
- systems that need validation-on-borrow, idle eviction, borrow timeouts, reference counting, or bounded-capacity acquisition semantics;
- values whose correctness depends on finalization-like guarantees.
In short: this package is for temporary reuse, not for persistent object management.
Ownership model ¶
The package assumes a strict ownership transfer model.
When Pool.Get returns a value, the caller becomes the logical owner of that value. The caller keeps that ownership until it calls Pool.Put. After Put returns, the caller MUST treat the value as no longer owned.
In particular, after Put returns the caller MUST NOT:
- continue mutating the value;
- read from the value as if it were still exclusively owned;
- publish the value to other goroutines as a still-live object;
- return the same borrowed instance to the pool again.
The default runtime does not attempt to detect double Put or use-after-Put misuse. Correct ownership discipline remains the responsibility of the caller.
Acquisition path ¶
The acquisition path is intentionally lean.
On Pool.Get:
- the backend is asked for a reusable value;
- if the backend has none, NewFunc is used to create one;
- the value is returned to the caller as-is.
Get does not reset values, re-run admission logic, or attach ownership metadata. The package relies on the invariant that values accepted into the backend were already reset before storage. This keeps the hot path short and predictable.
A value returned by Get is therefore always one of the following:
- a freshly constructed value produced by NewFunc; or
- a previously used value that was accepted for reuse and reset before it re-entered the backend.
Return path ¶
The return path is where lifecycle policy is applied.
On Pool.Put:
- ReuseFunc decides whether the value should be retained;
- if reuse is denied, DropFunc is invoked and the value is not stored;
- if reuse is allowed, ResetFunc prepares the value for the next user;
- the clean value is stored in the backend.
ResetFunc is therefore paid only on the accepted return path. This is a deliberate trade-off: retained values remain clean while idle, references may be released as early as possible, and Get remains minimal.
DropFunc is best-effort observation of explicit rejection-by-policy. It is not a finalizer, and it is not guaranteed to run for every value that ever becomes unreachable. Once a value has been accepted into a sync.Pool-style backend, that backend may later discard it without further notification.
Concurrency model ¶
A Pool is safe for concurrent use by multiple goroutines. Its lifecycle policy becomes immutable after construction, and backend storage is delegated to an internal sync.Pool-backed adapter.
However, the borrowed value returned by Get belongs to one logical owner at a time. The package does not make the borrowed value itself concurrency-safe. If T needs concurrent mutation after acquisition, that synchronization must be provided by T or by higher-level code.
The practical rule is:
- concurrent Get and Put calls on the same *Pool are supported;
- concurrent mutation of the same borrowed value is outside the package's guarantees unless the value type provides its own synchronization.
Zero values and construction ¶
The zero value of Pool is not ready for use.
This is intentional. A functioning pool requires:
- a constructor for slow-path allocation;
- a resolved lifecycle policy;
- an assembled backend.
As a result, New is the only supported public constructor.
The zero value of Options is also invalid because [Options.New] is required. Optional hooks such as Reset, Reuse, and OnDrop may be omitted; they are normalized internally when the pool is constructed.
Preferred shape of T ¶
T may be any type, but pointer-like temporary values are usually the best fit in performance-sensitive code.
Pointer-like T values are generally preferable because they:
- avoid copying large mutable state;
- align naturally with object-pooling usage patterns in Go;
- make ownership easier to reason about;
- usually interact better with sync.Pool-style reuse than large value objects do.
Value types are supported, but they should be chosen deliberately rather than by accident.
Relationship to [sync.Pool] ¶
The package intentionally follows the temporary-object model of sync.Pool. It does not attempt to hide that heritage.
In particular, callers should assume the same broad operational shape:
- previously returned values may later disappear from the backend without notice;
- the package does not promise stable retention of returned values;
- the package is designed for reducing allocation pressure for temporary values, not for maintaining a durable reserve of objects.
What this package adds on top of sync.Pool is not a different storage model, but a better public runtime contract:
- typed access through [Pool[T]];
- explicit lifecycle policy through Options;
- a fixed and testable return-path order;
- internal separation between policy, semantics, and backend storage.
Non-goals ¶
Package pool intentionally does not provide:
- validation-on-borrow or validation-on-return hooks;
- idle object eviction policies;
- borrow timeouts or blocking acquisition;
- reference counting;
- stable pool size guarantees;
- runtime ownership tracking in the default build;
- generic "resource management" abstractions beyond temporary value reuse.
If a caller needs those features, it likely needs a different kind of system than a sync.Pool-style temporary object reuse runtime.
Internal structure ¶
Although callers primarily interact with Pool and Options, the package is internally organized around a deliberately simple split:
- Options owns public lifecycle policy;
- lifecycle owns canonical return-path semantics;
- the internal backend owns only storage and retrieval;
- Pool owns public orchestration.
This separation keeps the external API small while allowing the internals to evolve conservatively without collapsing policy, semantics, and storage into one opaque implementation block.
Example ¶
The following example shows a typical pointer-like temporary object used as request-scoped scratch state:
type ParserState struct {
Input []byte
Offset int
Tokens []Token
Err error
}
states := pool.New(pool.Options[*ParserState]{
New: func() *ParserState {
return &ParserState{
Tokens: make([]Token, 0, 64),
}
},
Reset: func(s *ParserState) {
s.Input = nil
s.Offset = 0
s.Tokens = s.Tokens[:0]
s.Err = nil
},
Reuse: func(s *ParserState) bool {
return cap(s.Tokens) <= 4_096
},
})
state := states.Get()
defer states.Put(state)
// use state only within the current operation
The package is intended to change conservatively after stabilization. For that reason, its documentation is intentionally explicit and normative.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DropFunc ¶
type DropFunc[T any] func(T)
DropFunc observes values that were rejected for reuse.
DropFunc is invoked by Pool.Put only when ReuseFunc returns false. Typical uses are limited and low-level:
- optional metrics,
- debug-only accounting,
- release of external non-memory resources that do not belong in the ordinary reset path,
- tracing why objects are discarded.
DropFunc MUST be treated as a best-effort callback. It is not a lifecycle guarantee that a value will always pass through this hook before becoming unreachable. In particular, values obtained from a sync.Pool-style backend may disappear without an explicit callback once stored in the backend.
DropFunc SHOULD remain lightweight. Heavy logging, allocation, blocking I/O, or cross-goroutine coordination in this hook will often negate the benefit of pooling.
DropFunc MAY be nil in Options. In that case the pool performs no action on dropped values.
Example:
OnDrop: func(s *ParserState) {
metrics.ParserStateDrops.Add(1)
}
type NewFunc ¶
type NewFunc[T any] func() T
NewFunc constructs a new value for a Pool when the backend has no reusable instance available.
NewFunc is the only mandatory policy hook in Options. Without it, the pool has no way to materialize a value on the slow path.
The function MUST return a value that is immediately ready for use by the caller of Pool.Get. In other words, callers of Get SHOULD NOT be required to perform any additional initialization merely to make the returned value safe to touch.
Semantics:
- NewFunc is called lazily, only when the pool backend cannot provide a previously returned reusable value.
- The returned value becomes owned by the caller of Pool.Get.
- The value returned by NewFunc is not automatically reset by the pool; NewFunc itself is responsible for producing a valid initial state.
Performance guidance:
For most real-world use cases, T SHOULD be a pointer-like type such as *MyState, *RequestContext, or *Builder. Pointer-like values are typically the best fit for object pooling because they avoid copying large mutable state and align well with the intended usage pattern of sync.Pool-backed designs.
Example:
opts := pool.Options[*ParserState]{
New: func() *ParserState {
return &ParserState{
Tokens: make([]Token, 0, 64),
}
},
}
Example for a value type (supported, but usually less desirable for hot paths due to copying):
opts := pool.Options[SmallValue]{
New: func() SmallValue {
return SmallValue{}
},
}
type Options ¶
type Options[T any] struct { // New constructs a fresh value when the backend has no reusable instance // available. // // This field is required. [New] panics if Options.New is nil. // // New SHOULD return a value that is immediately valid for the caller of // [Pool.Get]. For pointer-like T, this usually means allocating the object // and initializing any slices, maps, or nested reusable fields to their // desired initial state. New NewFunc[T] // Reset prepares a value for reuse before it is stored back into the pool. // // If Reset is nil, the pool uses a no-op reset policy. // // Reset is called only when the reuse policy accepts the value. Reset ResetFunc[T] // Reuse decides whether a value should be retained for future reuse. // // If Reuse is nil, the pool uses an "always reuse" policy. // // Reuse is evaluated before Reset. When Reuse returns false, the value is // discarded and Reset is skipped. Reuse ReuseFunc[T] // OnDrop observes values that are explicitly rejected by Reuse. // // If OnDrop is nil, no drop callback is executed. // // OnDrop is not a general destruction hook. It is only invoked on the // explicit "rejected for reuse" path in [Pool.Put]. OnDrop DropFunc[T] }
Options defines the lifecycle policy for a typed Pool.
A pool built from Options follows this return path:
- Pool.Put receives a value.
- ReuseFunc decides whether the value should be retained.
- If reuse is denied, DropFunc is called and the value is discarded.
- If reuse is allowed, ResetFunc prepares the value for the next user.
- The clean value is stored in the backend for future Pool.Get calls.
The acquisition path is intentionally simpler:
- Pool.Get asks the backend for a reusable value.
- If none is available, NewFunc constructs one.
- The value is returned to the caller as-is.
Design notes:
- Options makes lifecycle policy explicit instead of encoding it into a mandatory interface implemented by T.
- This keeps domain types decoupled from the pool package.
- Different pools may apply different reuse policies to the same type.
Options values are configuration, not runtime state. They are consumed by New and resolved into an internal immutable policy set.
Zero value:
The zero value of Options is invalid because NewFunc is required. Callers MUST provide at least the [Options.New] field.
Recommended shape:
In performance-sensitive code, T SHOULD usually be a pointer-like type. This keeps Get/Put cheap, avoids copying large mutable state, and matches the common object-pooling pattern in Go.
Minimal example:
p := pool.New(pool.Options[*Builder]{
New: func() *Builder {
return &Builder{}
},
Reset: func(b *Builder) {
b.Reset()
},
})
Example with an explicit reuse policy:
p := pool.New(pool.Options[*RequestScratch]{
New: func() *RequestScratch {
return &RequestScratch{
Fields: make([]Field, 0, 32),
}
},
Reset: func(s *RequestScratch) {
s.Path = ""
s.Fields = s.Fields[:0]
s.Payload = s.Payload[:0]
},
Reuse: func(s *RequestScratch) bool {
return cap(s.Payload) <= 64<<10
},
OnDrop: func(s *RequestScratch) {
metrics.RequestScratchDrops.Add(1)
},
})
Example showing why callbacks are preferred over a mandatory interface on T:
// The same type can be reused with different retention policies.
fastPath := pool.New(pool.Options[*Frame]{
New: newFrame,
Reset: resetFrame,
Reuse: func(f *Frame) bool { return cap(f.Body) <= 8<<10 },
})
batchPath := pool.New(pool.Options[*Frame]{
New: newFrame,
Reset: resetFrame,
Reuse: func(f *Frame) bool { return cap(f.Body) <= 128<<10 },
})
These two pools can coexist even though they manage the same concrete type. If reuse logic were embedded directly in Frame, this separation would be much harder to express cleanly.
type Pool ¶
type Pool[T any] struct { // contains filtered or unexported fields }
Pool provides typed reuse of temporary values.
Pool is the primary public runtime type of this package. It combines three internal responsibilities into one externally simple API:
- value construction on the slow path, via [Options.New];
- lifecycle policy on return, via [Options.Reset], [Options.Reuse], and [Options.OnDrop];
- low-level storage and retrieval, via an internal sync.Pool-backed backend.
In practical terms, Pool exists so that callers can work with the following extremely small surface:
v := p.Get() ... use v as a temporary object ... p.Put(v)
without having to manually repeat reset, reuse admission, and backend interaction logic throughout the codebase.
Intended use ¶
Pool is intended for temporary reusable values that are frequently created, mutated within a single logical operation, and then returned for possible reuse. Typical examples include:
- parser or decoder state objects;
- request-scoped scratch structures;
- reusable builders, envelopes, or temporary frames;
- short-lived mutable helper structs used on hot paths.
Pool is usually not a good fit for:
- long-lived domain entities whose ownership escapes the current operation;
- values that must remain reachable after Put returns;
- objects requiring stable inventory guarantees or bounded-capacity borrow semantics;
- lifecycle models that need validation-on-borrow, idle eviction, reference counting, or blocking acquisition.
Design model ¶
Pool deliberately follows the temporary-object model of sync.Pool rather than the richer semantics of a full object-lifecycle manager. In particular:
- objects returned to the pool may later disappear from the backend without notice;
- the pool does not promise stable retention of previously returned values;
- the pool does not track borrow state or detect double Put misuse;
- the pool does not impose a mandatory interface on T.
The package instead keeps lifecycle policy explicit through Options. This allows the same type to be pooled under different reuse rules without forcing that type to embed reuse semantics directly into its own definition.
Ownership ¶
The caller owns a value obtained from Pool.Get until that value is passed to Pool.Put. After Put returns, the caller MUST treat the value as no longer owned. It must not be used, mutated, shared, or published as if it still belonged to the caller.
Pool does not attempt to enforce this rule at runtime in the default build. Correct ownership remains the responsibility of the caller.
Concurrency ¶
Pool is safe for concurrent use by multiple goroutines. Backend storage is delegated to sync.Pool through an internal adapter, and lifecycle policy is immutable after construction.
What is and is not concurrency-safe:
- concurrent calls to Get and Put on the same *Pool are supported;
- the value returned by Get belongs to one logical owner until Put;
- a borrowed value must not be concurrently mutated unless the value type T provides its own synchronization.
Zero value ¶
The zero value of Pool is not ready for use. Construct a pool with New. This is intentional because a valid pool requires a construction policy and a backend assembled from that policy.
Copying ¶
Pool values should be treated as configuration-bearing runtime handles and used through *Pool. New returns *Pool for this reason.
Although Pool mostly contains immutable policy references after construction, callers SHOULD NOT copy Pool values around by value. Doing so provides no benefit and obscures ownership of the runtime handle.
Performance notes ¶
The package is optimized for clarity of lifecycle and stable hot path shape, not for exotic specialization. The fast path of Pool.Get is a backend get. The fast path of Pool.Put is:
- evaluate reuse policy;
- optionally observe drop and return;
- reset accepted value;
- store accepted value.
There are no repeated nil-hook checks on the hot path because Options are resolved once during construction.
T may be any type, but pointer-like temporary values are usually the best fit. They avoid copying large mutable state and align with the intended use of pooling in Go.
Example
type ParserState struct {
Input []byte
Offset int
Tokens []Token
Err error
}
states := pool.New(pool.Options[*ParserState]{
New: func() *ParserState {
return &ParserState{
Tokens: make([]Token, 0, 64),
}
},
Reset: func(s *ParserState) {
s.Input = nil
s.Offset = 0
s.Tokens = s.Tokens[:0]
s.Err = nil
},
Reuse: func(s *ParserState) bool {
return cap(s.Tokens) <= 4_096
},
})
state := states.Get()
defer states.Put(state)
// use state within the current operation only
Internal structure ¶
Pool intentionally keeps its internal structure simple:
- backend owns only storage and retrieval;
- lifecycle owns only return-path semantics;
- Pool itself owns public orchestration.
This separation keeps the public runtime readable while still allowing the project to evolve internal implementation details conservatively.
The type is expected to change rarely once stabilized. As a result, comments in this file are intentionally explicit and normative.
func New ¶
New constructs a typed Pool from the supplied lifecycle policy.
New is the only supported constructor for Pool. It validates and resolves the public Options value, assembles the internal lifecycle controller, and creates the internal sync.Pool-backed backend.
Construction steps ¶
New performs the following work in order:
- validate and normalize Options via Options.resolve;
- construct the internal backend using the resolved New policy;
- construct the lifecycle controller using the resolved reset/reuse/drop policies;
- return the assembled *Pool.
Because optional hooks are normalized during construction, the returned Pool does not need to perform nil checks for Reset, Reuse, or OnDrop on every Put.
Panics ¶
New panics if [Options.New] is nil. This is a construction-time contract violation because the pool cannot materialize a value when the backend is empty without a creation policy.
New may also panic if the internal backend constructor contract is violated, though under normal use that cannot happen independently of options validation because the same resolved constructor is forwarded to the backend.
Example
p := pool.New(pool.Options[*Builder]{
New: func() *Builder {
return &Builder{}
},
Reset: func(b *Builder) {
b.Reset()
},
})
The returned pool is ready for concurrent Get/Put use immediately.
func (*Pool[T]) Get ¶
func (p *Pool[T]) Get() T
Get returns a temporary reusable value of type T.
Behaviour ¶
Get asks the internal backend for a previously returned reusable value. If no such value is currently available, the backend constructs one using the resolved NewFunc captured when the pool was created.
Get performs no lifecycle work beyond acquisition. In particular, Get does not:
- reset the value;
- validate that the value should still exist in the backend;
- decide whether the value should later be kept;
- attach ownership tracking metadata.
The value returned by Get is logically owned by the caller until Put is called.
State guarantees ¶
A value returned by Get is either:
- a newly constructed value produced by [Options.New]; or
- a previously returned value that was accepted for reuse and reset before being stored back into the backend.
In both cases, callers should reason about the value as ready for immediate use according to the invariants defined by their construction and reset policies.
Panics ¶
Get panics if called on a nil *Pool. This is treated as a hard misuse of the public runtime handle.
Example
v := p.Get() // mutate v within the current operation p.Put(v)
func (*Pool[T]) Put ¶
func (p *Pool[T]) Put(value T)
Put returns a value to the pool according to the configured lifecycle policy.
Canonical return path ¶
Put is the public entry point for the package's return-path semantics. It delegates the detailed order of operations to the internal lifecycle controller, which performs the following steps:
- evaluate the configured ReuseFunc;
- if reuse is denied, invoke DropFunc and stop;
- if reuse is allowed, invoke ResetFunc;
- store the cleaned value in the backend for possible future reuse.
This order is intentional and should not be changed casually. In particular, reuse admission happens before reset so that the admission policy can inspect the value in the state it actually accumulated during use.
Ownership ¶
After Put returns, the caller MUST treat the value as no longer owned, regardless of whether the value was retained or dropped. Put represents the end of the caller's lifecycle responsibility for that borrowed value.
What Put does not do ¶
Put does not:
- guarantee that the value will be retained indefinitely;
- detect double Put misuse in the default runtime;
- provide destruction/finalization semantics for dropped or later-evicted values;
- make the value safe for post-Put inspection by the caller.
Panics ¶
Put panics if called on a nil *Pool. This is treated as misuse of the public runtime handle.
Example
state := p.Get() defer p.Put(state) // use state during the current operation only
type ResetFunc ¶
type ResetFunc[T any] func(T)
ResetFunc prepares a value for future reuse before it is placed back into the pool.
ResetFunc is called by Pool.Put only after the pool has decided that the value is eligible for reuse. It is never called during Pool.Get. This is a deliberate design choice:
- objects retained by the pool remain in a clean state,
- references can be released as early as possible,
- the cost of reset is paid only on the return path,
- the acquisition path stays minimal and predictable.
ResetFunc SHOULD restore the value to the same logical state that callers would expect from a freshly created instance returned by NewFunc.
Typical reset work includes:
- clearing slices via s = s[:0],
- zeroing scalar fields,
- dropping transient references,
- resetting nested builders or temporary buffers,
- restoring invariants required by the next user of the object.
ResetFunc MUST NOT:
- return the value to another pool,
- publish the value to other goroutines,
- assume that it will never be called more than once over the lifetime of a reusable instance,
- perform ownership-sensitive logic that belongs to application code.
ResetFunc MAY be nil in Options. In that case the pool uses a no-op reset policy.
Example:
Reset: func(s *ParserState) {
s.Input = nil
s.Offset = 0
s.Tokens = s.Tokens[:0]
s.Err = nil
}
type ReuseFunc ¶
ReuseFunc decides whether a value is eligible to be stored for reuse.
ReuseFunc is evaluated by Pool.Put before ResetFunc is executed. If it returns false, the value is dropped instead of being reset and stored.
This hook exists because not every temporary object should be retained. Common examples include:
- a scratch structure that grew too large,
- an object that holds an oversized internal slice or map,
- an instance that entered a poisoned or non-reusable state,
- an object whose retained memory would be more expensive than allocating a fresh replacement later.
ReuseFunc SHOULD be:
- deterministic for a given object state,
- fast enough for the return path,
- free of side effects beyond reading the value.
ReuseFunc MUST NOT mutate the object. Mutation belongs in ResetFunc. Separating admission from reset keeps the control flow explicit and avoids subtle policy coupling.
ReuseFunc MAY be nil in Options. In that case the pool uses an "always reuse" policy.
Example:
Reuse: func(s *ParserState) bool {
return cap(s.Tokens) <= 4_096
}
Example for a request-scoped scratch object with a temporary byte field:
Reuse: func(r *RequestContext) bool {
return cap(r.Scratch) <= 64<<10
}
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
backend
Package backend contains internal storage backends used by the public pool runtime.
|
Package backend contains internal storage backends used by the public pool runtime. |
|
testutil
Package testutil contains shared helpers for repository-local tests and benchmarks.
|
Package testutil contains shared helpers for repository-local tests and benchmarks. |