Documentation
¶
Overview ¶
Package objpool provides typed object pools: Pool, a statically typed wrapper over sync.Pool, and FreeList, its GC-proof counterpart for few, large elements. A third piece, RatchetTrim, decides when a pooled element holds far more memory than its work uses, and should be dropped rather than reused.
The untyped sync.Pool leaves two things to every call site: the type assertion, and what to do when the pool is empty — and call sites that each handle those on their own diverge. Folding the element type into the pool makes Get return a ready *T, so a mis-asserted type is a compile error and the empty-pool path cannot be forgotten.
Choosing a pool ¶
Pool suits many small, cheap elements: like sync.Pool, it may drop idle elements at any time, so it never pins memory it no longer needs. FreeList suits few, large, expensive ones: it never drops what it is given, so reuse survives garbage collection, and its size is bounded by the peak number of elements checked out at once. It does not bound the memory each element retains; RatchetTrim decides when an element that a past burst inflated should be dropped instead of reused.
Element lifecycle ¶
Both pools take the same two functions, whose full contract New describes:
- newFn builds an element whenever Get has none to reuse. It must return a ready, non-nil element.
- resetFn, when non-nil, rewinds a recycled element before Get returns it. It never runs on a freshly built element, and an error from it drops the element in favor of a fresh build.
Get therefore never returns nil. Put ignores nil and does no work beyond parking the element; the caller must not use an element after putting it back.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type FreeList ¶
type FreeList[T any] struct { // contains filtered or unexported fields }
FreeList is the GC-proof counterpart of Pool, for elements that are few, large, and expensive to rebuild — an encoder whose buffers and compression state have grown to megabytes, say. A Pool silently defeats reuse for such elements once garbage collections outpace their use cycle: at a high allocation rate the collector runs several times per cycle, so the Pool rebuilds the element nearly every time, and the rebuilds' allocations feed the very collection rate that evicts it.
A FreeList never evicts, yet stays bounded without a cap: Get builds only when the list is empty, so the number of elements never exceeds the peak number checked out at once — for a fixed set of workers, the number of workers, whose memory the process must afford at peak anyway. The price is that memory stays at that peak: elements parked beyond current demand are kept, not released.
Nor does the list bound the memory each element retains. An element whose footprint can ratchet upward (a burst grows its buffers; quieter traffic never shrinks them) needs a trim policy at the caller, who drops the element instead of putting it back — the list keeps whatever it is given, forever. RatchetTrim is such a policy.
A FreeList is safe for concurrent use; the elements it hands out are not shared. Create one with NewFreeList: the zero value is not usable. A FreeList must not be copied after first use.
func NewFreeList ¶
NewFreeList returns a FreeList that builds elements with newFn, rewinds parked ones with resetFn, and parks at most maxIdle idle elements (maxIdle <= 0: no limit).
newFn and resetFn follow New's contract exactly, panics included. Reuse is the whole point of a FreeList, so a reset must keep the capacity the list exists to keep warm: truncating a buffer to length zero is right; replacing it with a new one defeats the list.
maxIdle is defense in depth on top of the structural bound (see FreeList): a Put that finds maxIdle elements already parked drops its element to the garbage collector. Set it comfortably above the peak number of elements checked out at once, because a cap below that quietly reintroduces the rebuild churn the list exists to prevent — visible as newFn calls that keep coming after warm-up. It never limits Get.
Example ¶
package main
import (
"bytes"
"fmt"
"runtime"
"github.com/JohanLindvall/objpool"
)
func main() {
builds := 0
buffers := objpool.NewFreeList(8,
func() *bytes.Buffer {
builds++
return bytes.NewBuffer(make([]byte, 0, 1<<20))
},
// Truncate, keeping the grown capacity the list exists to preserve.
func(b *bytes.Buffer) error { b.Reset(); return nil },
)
for i := 0; i < 3; i++ {
buf := buffers.Get()
buf.WriteString("payload")
buffers.Put(buf)
runtime.GC() // would empty a Pool; the FreeList keeps its element
}
fmt.Println("builds:", builds)
}
Output: builds: 1
func (*FreeList[T]) Get ¶
func (l *FreeList[T]) Get() *T
Get returns a parked element rewound by resetFn, or a freshly built one when the list is empty — never nil. Unlike Pool.Get, it hands back a parked element whenever one remains: the most recently parked, replaced by a fresh build only if its reset reports it unfit.
func (*FreeList[T]) Put ¶
func (l *FreeList[T]) Put(v *T)
Put parks v for reuse, or drops it when maxIdle elements are already parked; a nil v is ignored. Rewinding is Get's job (see NewFreeList), so v keeps its contents until it is next drawn. Putting the same element twice hands it out twice: exclusivity is the caller's to keep, as with Pool.
type Pool ¶
type Pool[T any] struct { // contains filtered or unexported fields }
Pool is a typed wrapper over sync.Pool: Get returns a ready *T — never nil, never needing a type assertion — and Put offers one back.
It keeps sync.Pool's semantics, including the one that matters most: Put is a hint, not storage. The runtime may drop a pooled element at any time, and drops every idle one within two garbage collections, so nothing whose loss would matter may live only in a Pool; see FreeList for elements too costly to lose.
A Pool is safe for concurrent use; the elements it hands out are not shared. Create one with New: the zero value is not usable. A Pool must not be copied after first use.
func New ¶
New returns a Pool that builds elements with newFn and rewinds recycled ones with resetFn.
newFn runs whenever Get finds the pool empty, on the caller's goroutine, and may capture whatever construction needs. It must return a ready, non-nil element.
resetFn, when non-nil, rewinds a recycled element before Get hands it out, which is what lets Get promise a clean element instead of leaving each call site to remember. It runs in Get rather than in Put deliberately: a reset that fails can then be answered with a fresh element, whereas in Put there is no caller left to tell. It never runs on a freshly built element — newFn already returns a ready one — so required setup belongs in newFn, not resetFn. Pass nil when there is nothing to rewind, or when rewinding needs an argument only the call site has: a pooled gzip.Writer, say, is reset onto its destination by the caller after Get.
A resetFn that returns an error declares the element unfit to reuse: Get drops it and builds a replacement, so the error costs one rebuild and never reaches the caller. That path exists so that a reset which can fail never hands back a half-rewound element.
Get's never-nil guarantee rests on newFn, so both ways of breaking it panic early, with a message naming the cause: New panics if newFn is nil — at construction, typically program start, rather than on the first miss under load — and Get panics if newFn returns nil, rather than handing back a nil that dereferences in unrelated code. Neither check touches a pool hit.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/JohanLindvall/objpool"
)
func main() {
buffers := objpool.New(
func() *bytes.Buffer { return new(bytes.Buffer) },
func(b *bytes.Buffer) error { b.Reset(); return nil },
)
buf := buffers.Get() // a ready *bytes.Buffer: never nil, no type assertion
buf.WriteString("hello")
fmt.Println(buf)
buffers.Put(buf)
// Recycled and rewound, or freshly built: either way it comes back empty.
fmt.Println(buffers.Get().Len())
}
Output: hello 0
type RatchetTrim ¶
type RatchetTrim struct {
// Floor exempts elements retaining fewer bytes: no cycle below it is
// wasteful.
Floor int
// Factor is the retained/used ratio above which a cycle is wasteful; a
// Factor <= 0 makes every cycle at or above Floor wasteful.
Factor int
// Strikes is how many consecutive wasteful cycles make Due report drop;
// Strikes <= 0 disables the policy, which is why the zero value never trims.
Strikes int
// contains filtered or unexported fields
}
RatchetTrim is a drop policy for pooled elements whose retained memory can ratchet upward: a burst grows an element's buffers, the quieter traffic after it never uses that capacity again, and a FreeList keeps it forever. The decision belongs to the caller at release: keep one RatchetTrim in each pooled element, call Due as the element is released, and drop the element instead of putting it back when Due reports true.
A cycle is wasteful when the element retains at least Floor bytes and more than Factor times what the cycle used; Strikes consecutive wasteful cycles make Due report drop. The floor exempts elements small enough that a rebuild would cost more than the memory it reclaims — size alone, regardless of how busy the element is. The strikes forgive a burst quickly, while a footprint the traffic after it never uses is released within a few cycles.
The zero value never trims; DefaultRatchetTrim returns a tuned policy. A RatchetTrim is not safe for concurrent use: like the element it lives on, it has one holder at a time.
Example ¶
package main
import (
"fmt"
"github.com/JohanLindvall/objpool"
)
func main() {
// job is a pooled element whose buffer grows to fit the largest input it has seen.
type job struct {
buf []byte
trim objpool.RatchetTrim
}
jobs := objpool.NewFreeList(0,
func() *job {
// A small policy to keep the example light; DefaultRatchetTrim suits
// elements that grow to megabytes.
return &job{trim: objpool.RatchetTrim{Floor: 1 << 10, Factor: 4, Strikes: 3}}
},
func(j *job) error { j.buf = j.buf[:0]; return nil },
)
// release puts j back, or drops it once its buffer has sat mostly unused for
// Strikes consecutive cycles. It reports whether j was kept.
release := func(j *job) bool {
if j.trim.Due(cap(j.buf), len(j.buf)) {
return false // dropped: the garbage collector reclaims the buffer
}
jobs.Put(j)
return true
}
j := jobs.Get()
j.buf = append(j.buf, make([]byte, 64<<10)...) // a burst grows the buffer to 64 KiB
fmt.Println("burst: kept =", release(j))
for i := 1; i <= 3; i++ {
j = jobs.Get()
j.buf = append(j.buf, "small"...) // quiet traffic uses a sliver of it
fmt.Printf("quiet %d: kept = %v\n", i, release(j))
}
}
Output: burst: kept = true quiet 1: kept = true quiet 2: kept = true quiet 3: kept = false
func DefaultRatchetTrim ¶
func DefaultRatchetTrim() RatchetTrim
DefaultRatchetTrim returns a policy that drops an element retaining at least 16 MiB once it has used less than a quarter of its footprint for 8 consecutive cycles.
func (*RatchetTrim) Due ¶
func (t *RatchetTrim) Due(retained, used int) bool
Due records one cycle and reports whether the element should be dropped rather than put back. retained is the element's kept footprint in bytes; used is what the cycle's work actually needed. One honest cycle — capacity genuinely used — resets the count, so sustained heavy traffic never cycles elements.
A cycle with used == 0 is the most wasteful case, not an exemption: an element above the floor that did no work for Strikes consecutive cycles holds its memory for no benefit at all. That cannot punish an element for sitting idle in a FreeList: Due runs only when a holder releases the element, never while it is parked.