pool

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 24, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

README

arcoris.dev/pool

Typed temporary-object reuse for Go hot paths.

Start Here Go Package Contract Performance

Quick Start · Core Model · Intended Use · Docs Index · Contributing · Security · Performance

Policy-driven reuse · Explicit ownership transfer · Canonical return-path semantics · Benchmark-first engineering

Read by goal: Start here · Lifecycle semantics · Architecture · Non-goals · Performance evidence

README.md is the public landing page. docs/index.md is the repository documentation index, and doc.go remains the package contract for Go users and pkg.go.dev.

arcoris.dev/pool is a small Go package for typed reuse of temporary mutable values. It stays close to the sync.Pool mental model, but keeps construction, reset, reuse admission, and drop observation explicit as policy callbacks instead of turning temporary-object reuse into a larger framework.

Start here

If you want to... Read
understand the package in one screen Quick start and Core model
learn the exact ownership and return-path contract Lifecycle guide
understand repository structure and boundaries Architecture guide
see what the package intentionally excludes Non-goals guide
find contributor, reporting, or repository policy paths Documentation
inspect benchmarks, charts, and curated reports Performance overview

Why it exists

In hot code paths, temporary mutable state usually ends up in one of two shapes:

  • repeated fresh allocation of scratch objects; or
  • direct sync.Pool usage scattered through the codebase, with reset, admission, and drop logic repeated manually.

arcoris.dev/pool keeps the temporary-object model close to sync.Pool, but makes the lifecycle policy explicit in one place:

  • New defines how values are constructed on a miss;
  • Reset defines how accepted values are cleaned before storage;
  • Reuse decides whether a returned value is worth keeping;
  • OnDrop observes explicit rejection by policy.

Quick start

package main

import "arcoris.dev/pool"

type Builder struct {
	buf []byte
}

func main() {
	p := pool.New(pool.Options[*Builder]{
		New: func() *Builder {
			return &Builder{
				buf: make([]byte, 0, 1024),
			}
		},
		Reset: func(b *Builder) {
			b.buf = b.buf[:0]
		},
		Reuse: func(b *Builder) bool {
			return cap(b.buf) <= 1024
		},
	})

	v := p.Get()
	v.buf = append(v.buf, "hello"...)

	// Ownership ends here. The caller must not use v after Put returns.
	p.Put(v)
}

Core model

The package is built around three layers:

  • Options[T] defines lifecycle policy;
  • Pool[T] exposes the public Get/Put runtime;
  • an internal backend stores already-clean reusable values.

The return path is canonical:

  1. evaluate reuse admission;
  2. if reuse is denied, invoke OnDrop and stop;
  3. if reuse is allowed, invoke Reset;
  4. store the cleaned value in the backend.

This ordering is part of the package contract. Admission runs before reset so policy can inspect the real post-use state. Reset runs before backend storage so retained values stay clean while idle.

Ownership and concurrency

Ownership is explicit:

  • after Get, the caller owns the value;
  • after Put, the caller must treat the value as no longer owned.

A value must not be used after Put returns.

Pool[T] itself is safe for concurrent use by multiple goroutines. That does not make the borrowed value automatically safe for concurrent use. Any concurrency properties of T remain the responsibility of the caller and the type.

Choosing T

The package usually works best with pointer-like mutable values.

Why:

  • the runtime avoids copying larger state around;
  • reset can happen in place;
  • the usage model aligns well with temporary-object pooling in Go;
  • performance benefits are easier to justify.

Value types are supported, but large or frequently copied values should be benchmarked carefully. The repository benchmark layer already includes explicit pointer-like versus value-shape coverage.

Intended use

This package is a good fit for:

  • parser or decoder state;
  • request-scoped scratch structures;
  • builders, envelopes, and reusable work records;
  • pointer-like temporary objects on hot paths.

It is usually worth considering when:

  • values are created frequently;
  • one borrowed value belongs to one logical operation at a time;
  • reset is cheaper than repeated reconstruction;
  • reuse policy benefits from being explicit and local.

Non-goals

arcoris.dev/pool is intentionally not:

  • a general resource manager;
  • a borrow or lease tracker;
  • a stable object inventory or cache;
  • a queue, scheduler, or semaphore;
  • a validation framework;
  • a specialized memory or buffer pool;
  • a rich commons-pool-style framework.

The package also does not promise:

  • stable retention of returned values;
  • borrow tracking in the default runtime;
  • zero allocations for every T;
  • that pooling is beneficial for all shapes of T.

Documentation

Document Use when Focus
Docs index you want the repository map first the best entry point into the documentation set
Package contract you want the Go-facing package contract exported API intent and runtime model
Contributing guide you want contributor workflow and validation expectations PR shape, validation, docs sync, and performance evidence rules
Security policy you need the repository's vulnerability reporting and security scope guidance private reporting path, supported versions, and repo-specific security boundaries
Code of Conduct you want the repository's collaboration and moderation baseline expected behavior, reporting path, and review standards
Third-Party Notices you need attribution and third-party notice status bundled or adapted upstream material and pinned tooling references
Architecture guide you want the structural view layers, boundaries, repository layout
Lifecycle guide you need precise runtime semantics ownership, acquisition, return-path invariants
Non-goals guide you are evaluating scope explicit exclusions and product boundaries
Performance overview you want the evidence trail benchmarks, charts, methodology, reports
Initial baseline report you want the current curated snapshot current benchmark interpretation and chart links

Performance overview

The repository keeps benchmark, profile, chart, and report layers as first-class engineering artifacts. Snapshot charts are presentation summaries over raw benchmark evidence, not replacements for the underlying artifacts.

Initial baseline package baseline time chart

Initial baseline package baseline allocs chart

Use the performance overview for the full artifact workflow and the initial baseline report for the current curated snapshot.

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:

  1. acquire a temporary value of type T;
  2. use that value within one logical operation;
  3. return the value for possible reuse;
  4. 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:

  1. Options, which describes lifecycle policy declaratively;
  2. Pool, which exposes the public Get/Put runtime API;
  3. 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:

  1. evaluate the reuse admission policy;
  2. if reuse is denied, invoke the drop callback and stop;
  3. if reuse is allowed, reset the value into a clean reusable state;
  4. 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:

  1. the backend is asked for a reusable value;
  2. if the backend has none, NewFunc is used to create one;
  3. 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:

  1. ReuseFunc decides whether the value should be retained;
  2. if reuse is denied, DropFunc is invoked and the value is not stored;
  3. if reuse is allowed, ResetFunc prepares the value for the next user;
  4. 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:

  1. Pool.Put receives a value.
  2. ReuseFunc decides whether the value should be retained.
  3. If reuse is denied, DropFunc is called and the value is discarded.
  4. If reuse is allowed, ResetFunc prepares the value for the next user.
  5. The clean value is stored in the backend for future Pool.Get calls.

The acquisition path is intentionally simpler:

  1. Pool.Get asks the backend for a reusable value.
  2. If none is available, NewFunc constructs one.
  3. 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:

  1. value construction on the slow path, via [Options.New];
  2. lifecycle policy on return, via [Options.Reset], [Options.Reuse], and [Options.OnDrop];
  3. 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:

  1. evaluate reuse policy;
  2. optionally observe drop and return;
  3. reset accepted value;
  4. 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

func New[T any](options Options[T]) *Pool[T]

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:

  1. validate and normalize Options via Options.resolve;
  2. construct the internal backend using the resolved New policy;
  3. construct the lifecycle controller using the resolved reset/reuse/drop policies;
  4. 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:

  1. evaluate the configured ReuseFunc;
  2. if reuse is denied, invoke DropFunc and stop;
  3. if reuse is allowed, invoke ResetFunc;
  4. 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

type ReuseFunc[T any] func(T) bool

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL