A small, typed memory arena for Go.
memoryArena pre-allocates a fixed-size backing slice and hands out values from it by advancing an offset. Instead of allocating many short-lived objects individually on the heap, you can allocate them from an arena and reuse the whole backing buffer with Reset.
import "github.com/Protocol-Lattice/memoryArena"Go’s garbage collector is excellent, but some workloads create large numbers of short-lived temporary values. Examples include parsers, request-scoped buffers, simulation ticks, batch processing, and hot loops.
An arena changes the ownership model:
- allocate many values from one backing buffer;
- reuse memory all at once with
Reset; - avoid individual object lifetime management;
- reduce garbage collector pressure in scoped workloads.
This package is intentionally safe and Go-native. It uses typed []T storage instead of raw unsafe.Pointer memory.
- Generic
MemoryArena[T]backed by[]T. - Fast single-value allocation with
Alloc. - Non-panicking allocation with
TryAlloc. - Contiguous slab allocation with
AllocSlab. - Non-panicking slab allocation with
TryAllocSlab. - Copy-in slab allocation with
AllocSlabWith. - Non-panicking copy-in slab allocation with
TryAllocSlabWith. - Capacity helpers:
Used,Remaining, andCap. Resetclears only the allocated region before reuse.ConcurrentMemoryArena[T]for synchronized shared allocation.- Context-aware concurrent allocation methods.
PooledMemoryArena[T]for high-concurrency scoped workloads.- Context helpers for request-scoped arena ownership.
- HTTP request helpers for injecting and extracting arenas from
*http.Request.
go get github.com/Protocol-Lattice/memoryArenapackage main
import (
"fmt"
"github.com/Protocol-Lattice/memoryArena"
)
func main() {
arena := memoryArena.NewMemoryArena[int](4)
first := arena.Alloc(10)
second := arena.Alloc(20)
fmt.Println(*first)
fmt.Println(*second)
arena.Reset()
next := arena.Alloc(30)
fmt.Println(*next)
}arena := memoryArena.NewMemoryArena[int](1024)This creates an arena that can hold 1024 values of type int.
The arena is fixed-size. If you allocate past capacity with Alloc, it panics. Use the Try* APIs when you want explicit capacity handling.
arena := memoryArena.NewMemoryArena[string](2)
name := arena.Alloc("kamil")
fmt.Println(*name)Alloc copies the value into the next free slot and returns a pointer to that slot.
ptr := arena.Alloc("hello")The returned pointer points directly into the arena buffer.
Use TryAlloc when capacity exhaustion should be handled manually.
arena := memoryArena.NewMemoryArena[int](2)
first, ok := arena.TryAlloc(10)
if !ok {
return
}
second, ok := arena.TryAlloc(20)
if !ok {
return
}
third, ok := arena.TryAlloc(30)
if !ok {
fmt.Println("arena is full")
}
_, _, _ = first, second, thirdFailed Try* allocations do not advance the arena offset.
arena := memoryArena.NewMemoryArena[int](4)
fmt.Println(arena.Cap()) // 4
fmt.Println(arena.Used()) // 0
fmt.Println(arena.Remaining()) // 4
_ = arena.Alloc(10)
fmt.Println(arena.Used()) // 1
fmt.Println(arena.Remaining()) // 3Use AllocSlab when you need a contiguous region inside the arena.
arena := memoryArena.NewMemoryArena[int](8)
slab := arena.AllocSlab(3)
slab[0] = 10
slab[1] = 20
slab[2] = 30
next := arena.Alloc(40)
fmt.Println(slab)
fmt.Println(*next)The returned slab is capacity-bounded, so appending to it cannot silently overwrite later arena allocations.
slab := arena.AllocSlab(3)
// cap(slab) == len(slab)arena := memoryArena.NewMemoryArena[int](2)
slab, ok := arena.TryAllocSlab(4)
if !ok {
fmt.Println("slab does not fit")
return
}
_ = slabUse AllocSlabWith when you already have values to copy into the arena.
arena := memoryArena.NewMemoryArena[string](4)
names := arena.AllocSlabWith("alice", "bob", "carol")
fmt.Println(names)Use TryAllocSlabWith when you want non-panicking behavior.
arena := memoryArena.NewMemoryArena[string](2)
names, ok := arena.TryAllocSlabWith("alice", "bob", "carol")
if !ok {
fmt.Println("values do not fit")
return
}
_ = namesReset clears the allocated portion of the arena and moves the offset back to zero.
arena := memoryArena.NewMemoryArena[int](2)
first := arena.Alloc(10)
second := arena.Alloc(20)
arena.Reset()
fmt.Println(*first) // 0
fmt.Println(*second) // 0
next := arena.Alloc(30)
fmt.Println(*next) // 30Pointers and slabs returned before Reset still point into the backing buffer, but their previous values are no longer live.
Treat old pointers and slabs as invalid after reset.
The preferred usage is strongly typed:
arena := memoryArena.NewMemoryArena[int](16)If you need mixed values, you can use any:
arena := memoryArena.NewMemoryArena[any](4)
a := arena.Alloc(123)
b := arena.Alloc("hello")
fmt.Println((*a).(int))
fmt.Println((*b).(string))This allows heterogeneous storage, but the returned pointers are *any, so reading values requires type assertions.
value := (*a).(int)For maximum type safety and performance, prefer concrete T arenas when possible.
MemoryArena[T] is intentionally not thread-safe. It is designed for single-owner hot paths.
For concurrent workloads, use one of these models:
| Type | Best for | Synchronization model |
|---|---|---|
MemoryArena[T] |
Single-owner hot paths | None |
ConcurrentMemoryArena[T] |
Shared arena across goroutines | Single semaphore gate |
PooledMemoryArena[T] |
Request/job scoped concurrent workloads | sync.Pool of isolated arenas |
ConcurrentMemoryArena[T] wraps a regular arena with a single semaphore gate. All allocations and resets are synchronized.
package main
import (
"sync"
"github.com/Protocol-Lattice/memoryArena"
)
func main() {
arena := memoryArena.NewConcurrentMemoryArena[int](1024)
var wg sync.WaitGroup
for workerID := 0; workerID < 8; workerID++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for i := 0; i < 100; i++ {
value := workerID*100 + i
ptr := arena.Alloc(value)
_ = ptr
}
}(workerID)
}
wg.Wait()
}The synchronized arena protects allocation and reset operations. It does not protect later reads or writes through returned pointers and slices.
If multiple goroutines access the same returned pointer or slab, coordinate that access yourself.
Use context-aware methods when waiting for the arena gate should respect cancellation or deadlines.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
arena := memoryArena.NewConcurrentMemoryArena[int](1024)
ptr, err := arena.AllocContext(ctx, 42)
if err != nil {
return err
}
_ = ptrContext-aware slab allocation is also available:
slab, err := arena.AllocSlabContext(ctx, 4)
if err != nil {
return err
}
_ = slabCopy-in slab allocation:
slab, err := arena.AllocSlabWithContext(ctx, 1, 2, 3, 4)
if err != nil {
return err
}
_ = slabContext-aware reset:
if err := arena.ResetContext(ctx); err != nil {
return err
}PooledMemoryArena[T] is designed for high-concurrency scoped workloads.
Instead of many goroutines fighting over one shared arena, each Execute call checks out an isolated arena from a sync.Pool, runs your function, resets the arena, and returns it to the pool.
pool := memoryArena.NewPooledMemoryArena[int](256)
pool.Execute(func(arena *memoryArena.MemoryArena[int]) {
ptr := arena.Alloc(123)
slab := arena.AllocSlabWith(1, 2, 3, 4)
_, _ = ptr, slab
})Values allocated inside the closure should not escape the closure.
Good use cases:
- HTTP request handlers;
- worker jobs;
- parser passes;
- temporary batch processing;
- high-concurrency loops.
Execute resets the arena even if the closure panics. The panic still bubbles up to the caller.
You can attach an arena to a context.Context.
ctx := context.Background()
arena := memoryArena.NewMemoryArena[int](128)
ctx = memoryArena.Inject(ctx, arena)
got, err := memoryArena.Extract[int](ctx)
if err != nil {
return err
}
_ = gotIf no arena exists, the stored arena is nil, or the generic type does not match, Extract returns ErrNoArena.
got, err := memoryArena.Extract[string](ctx)
if errors.Is(err, memoryArena.ErrNoArena) {
// no string arena found
}
_ = gotFor performance-sensitive code, prefer passing *MemoryArena[T] explicitly instead of storing it in context.
Use InjectRequest and ExtractRequest for request-scoped arena ownership.
func handler(w http.ResponseWriter, r *http.Request) {
arena := memoryArena.NewMemoryArena[int](128)
r = memoryArena.InjectRequest(r, arena)
got, err := memoryArena.ExtractRequest[int](r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = got
}InjectRequest returns a cloned request with the arena attached to the request context. It does not mutate the original request.
func NewMemoryArena[T any](size uint) *MemoryArena[T]Creates a fixed-size arena for values of type T.
func (arena *MemoryArena[T]) Alloc(obj T) *TCopies obj into the next free slot and returns a pointer to it. Panics if the arena is full.
func (arena *MemoryArena[T]) TryAlloc(obj T) (*T, bool)Copies obj into the next free slot. Returns nil, false if the arena is full.
func (arena *MemoryArena[T]) AllocSlab(size uint) []TReserves size contiguous slots and returns them as a capacity-bounded slice. Panics if the slab does not fit.
func (arena *MemoryArena[T]) TryAllocSlab(size uint) ([]T, bool)Reserves size contiguous slots. Returns nil, false if the slab does not fit.
func (arena *MemoryArena[T]) AllocSlabWith(values ...T) []TReserves a slab, copies values into it, and returns the allocated slab. Panics if the values do not fit.
func (arena *MemoryArena[T]) TryAllocSlabWith(values ...T) ([]T, bool)Reserves a slab, copies values into it, and returns the allocated slab. Returns nil, false if the values do not fit.
func (arena *MemoryArena[T]) Reset()Clears the allocated region and resets the offset to zero.
func (arena *MemoryArena[T]) Used() uintReturns the number of allocated slots.
func (arena *MemoryArena[T]) Remaining() uintReturns the number of available slots.
func (arena *MemoryArena[T]) Cap() uintReturns the total arena capacity.
func NewConcurrentMemoryArena[T any](size uint) *ConcurrentMemoryArena[T]Creates a synchronized arena.
func (arena *ConcurrentMemoryArena[T]) Alloc(obj T) *TSynchronizes access, allocates one value, and returns a pointer to it.
func (arena *ConcurrentMemoryArena[T]) AllocContext(ctx context.Context, obj T) (*T, error)Attempts to acquire the arena gate using ctx, then allocates one value.
func (arena *ConcurrentMemoryArena[T]) AllocSlab(size uint) []TSynchronizes access and allocates a contiguous slab.
func (arena *ConcurrentMemoryArena[T]) AllocSlabContext(ctx context.Context, size uint) ([]T, error)Attempts to acquire the arena gate using ctx, then allocates a contiguous slab.
func (arena *ConcurrentMemoryArena[T]) AllocSlabWith(values ...T) []TSynchronizes access, reserves a slab, and copies values into it.
func (arena *ConcurrentMemoryArena[T]) AllocSlabWithContext(ctx context.Context, values ...T) ([]T, error)Attempts to acquire the arena gate using ctx, reserves a slab, and copies values into it.
func (arena *ConcurrentMemoryArena[T]) Reset()Synchronizes access and resets the underlying arena.
func (arena *ConcurrentMemoryArena[T]) ResetContext(ctx context.Context) errorAttempts to acquire the arena gate using ctx, then resets the underlying arena.
func NewPooledMemoryArena[T any](arenaSize uint) *PooledMemoryArena[T]Creates a pool of reusable arenas.
func (pool *PooledMemoryArena[T]) Execute(action func(arena *MemoryArena[T]))Checks out an arena, runs action, resets the arena, and returns it to the pool.
func Inject[T any](ctx context.Context, arena *MemoryArena[T]) context.ContextReturns a child context containing the arena.
func Extract[T any](ctx context.Context) (*MemoryArena[T], error)Extracts a typed arena from context.
func InjectRequest[T any](r *http.Request, arena *MemoryArena[T]) *http.RequestReturns a cloned request with the arena injected into its context.
func ExtractRequest[T any](r *http.Request) (*MemoryArena[T], error)Extracts a typed arena from the request context.
MemoryArena[T]is not thread-safe.- Use
ConcurrentMemoryArena[T]if multiple goroutines must allocate from one shared arena. - Use
PooledMemoryArena[T]when each task/request can use an isolated temporary arena. - Returned pointers and slabs point directly into the arena buffer.
- Do not use returned values after
Reset. - Do not use values allocated inside
PooledMemoryArena.Executeafter the closure returns. Resetclears allocated slots, so old pointers may observe zero values.AllocSlabandAllocSlabWithreturn capacity-bounded slices.- Failed
Try*allocations do not advance the offset. - Arena capacity is fixed.
- Use
MemoryArena[any]only when heterogeneous storage is needed and type assertions are acceptable.
Run all benchmarks:
go test -bench=. -benchmemRun tests with the race detector:
go test -race ./...Run all tests:
go test ./...The benchmark suite covers:
- heap allocation vs arena allocation;
- reset-heavy reuse;
- slab allocation;
- heap slice allocation vs arena slab allocation;
- GC pressure comparisons;
- contended concurrent allocation;
- contended concurrent slab allocation;
- pooled arena execution under parallel contention.
Use memoryArena when:
- values have a shared scoped lifetime;
- you can reset all temporary values together;
- you want typed arena-style allocation without unsafe raw memory;
- you want fewer heap allocations in hot paths;
- you are building parsers, request-scoped buffers, simulations, temporary batches, or worker-local scratch memory.
Do not use an arena when:
- values need independent lifetimes;
- values must be freed individually;
- values escape unpredictably;
- capacity is unknown and unbounded;
- regular heap allocation is already simple and fast enough;
- the code becomes harder to reason about because of reset-based lifetime management.
MIT