memoize

package module
v0.0.0-...-a0032c4 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

memoize

memoize is a small, type-safe, in-process operation cache for Go services. It requires Go 1.27 or later and uses only the standard library.

Warning: This project is in early development.

Usage

type Service struct {
	findUser memoize.Func2[string, string, User]
}

func NewService(repository Repository) *Service {
	memo := memoize.MustNew(memoize.Policy{
		TTL:      30 * time.Second,
		Capacity: 10_000,
		Jitter:   3 * time.Second,
	})

	return &Service{
		findUser: memo.Func2(
			repository.FindUser,
			memoize.Named("users.find"), // optional
		),
	}
}

func (s *Service) User(ctx context.Context, tenantID, userID string) (User, error) {
	return s.findUser.Call(ctx, tenantID, userID)
}

func (s *Service) UserChanged(tenantID, userID string) {
	s.findUser.Forget(tenantID, userID)
}

Once, Func, Func2, Func3, and Func4 accept ordinary context-aware functions. Go infers their argument and result types. Operations provide Call, Refresh, Clear, Stats, and Name; keyed operations also provide Forget. A Call method value is an ordinary function value suitable for dependency injection.

Names are inferred once from the loader's runtime symbol. Use Named for a stable telemetry name. The inferred or overridden name is only a display name; it never determines operation identity or cache correctness. Statistics are reported per operation. A Memoizer must be constructed with New or MustNew; constructed values are immutable and safe to copy.

Custom keys

The default methods use static comparable tuple keys. Use the corresponding By method when arguments are not comparable or Go equality is not the right semantic equality:

search := memo.Func2By(
	client.Search,
	func(tenant string, request SearchRequest) searchKey {
		return searchKey{
			Tenant: tenant,
			Query:  request.Query,
			Page:   request.Page,
		}
	},
)

The key function is applied consistently by Call, Refresh, and Forget.

Error policies

Successful values are cached by default. Selected negative caching and stale-if-error are opt-in operation options:

find := memo.Func(
	repository.Find,
	memoize.StaleIfError(2*time.Minute),
	memoize.CacheErrorsFor(5*time.Second, func(err error) bool {
		return errors.Is(err, repository.ErrNotFound)
	}),
)

Selected errors cache the exact value/error pair. context.Canceled, context.DeadlineExceeded, ErrRecursiveLoad, ErrLoaderGoexit, and *PanicError are never negative-cached.

After the normal TTL, a successful value may remain eligible for stale fallback for the configured duration. An ordinary Call can return it when a new load fails with an unselected error; eligibility is rechecked when that load finishes. Refresh bypasses a fresh result, coalesces with another load for the key, publishes successful and selected-error results, and always returns the actual loader error instead of stale data.

Behavior

Each operation has an independent TTL, exact LRU capacity, and statistics. Expiration is lazy. Concurrent misses for one key coalesce, while callers cancel independently. Forget and Clear prevent an older in-progress load from repopulating the cache.

Keys use Go equality and must be comparable and reflexive. Context-derived inputs that affect results belong in the key. Cached reference-bearing values are shared and should be treated as immutable.

See DESIGN.md for detailed concurrency, invalidation, expiration, recursion, and accounting semantics.

Documentation

Overview

Package memoize provides bounded, concurrent memoization for operations.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrLoaderGoexit is returned when a loader terminates its goroutine with
	// runtime.Goexit.
	ErrLoaderGoexit = errors.New("memoize: loader called runtime.Goexit")

	// ErrRecursiveLoad is returned when a load waits on a flight already in its
	// propagated loader-context chain.
	ErrRecursiveLoad = errors.New("memoize: recursive load")
)

Functions

This section is empty.

Types

type Func

type Func[A, V any] struct {
	// contains filtered or unexported fields
}

Func is a memoized one-argument operation.

func (Func[A, V]) Call

func (o Func[A, V]) Call(ctx context.Context, a A) (V, error)

Call returns the memoized result.

func (Func[A, V]) Clear

func (o Func[A, V]) Clear()

Clear invalidates every result and in-progress generation.

func (Func[A, V]) Forget

func (o Func[A, V]) Forget(a A)

Forget invalidates the result for a.

func (Func[A, V]) Name

func (o Func[A, V]) Name() string

Name returns the inferred or configured display name.

func (Func[A, V]) Refresh

func (o Func[A, V]) Refresh(ctx context.Context, a A) (V, error)

Refresh reloads the result even if a fresh result exists.

func (Func[A, V]) Stats

func (o Func[A, V]) Stats() Stats

Stats returns operation statistics.

type Func2

type Func2[A, B, V any] struct {
	// contains filtered or unexported fields
}

Func2 is a memoized two-argument operation.

func (Func2[A, B, V]) Call

func (o Func2[A, B, V]) Call(ctx context.Context, a A, b B) (V, error)

Call returns the memoized result.

func (Func2[A, B, V]) Clear

func (o Func2[A, B, V]) Clear()

Clear invalidates every result and in-progress generation.

func (Func2[A, B, V]) Forget

func (o Func2[A, B, V]) Forget(a A, b B)

Forget invalidates the result for the arguments.

func (Func2[A, B, V]) Name

func (o Func2[A, B, V]) Name() string

Name returns the inferred or configured display name.

func (Func2[A, B, V]) Refresh

func (o Func2[A, B, V]) Refresh(ctx context.Context, a A, b B) (V, error)

Refresh reloads the result even if a fresh result exists.

func (Func2[A, B, V]) Stats

func (o Func2[A, B, V]) Stats() Stats

Stats returns operation statistics.

type Func3

type Func3[A, B, C, V any] struct {
	// contains filtered or unexported fields
}

Func3 is a memoized three-argument operation.

func (Func3[A, B, C, V]) Call

func (o Func3[A, B, C, V]) Call(ctx context.Context, a A, b B, c C) (V, error)

Call returns the memoized result.

func (Func3[A, B, C, V]) Clear

func (o Func3[A, B, C, V]) Clear()

Clear invalidates every result and in-progress generation.

func (Func3[A, B, C, V]) Forget

func (o Func3[A, B, C, V]) Forget(a A, b B, c C)

Forget invalidates the result for the arguments.

func (Func3[A, B, C, V]) Name

func (o Func3[A, B, C, V]) Name() string

Name returns the inferred or configured display name.

func (Func3[A, B, C, V]) Refresh

func (o Func3[A, B, C, V]) Refresh(
	ctx context.Context,
	a A,
	b B,
	c C,
) (V, error)

Refresh reloads the result even if a fresh result exists.

func (Func3[A, B, C, V]) Stats

func (o Func3[A, B, C, V]) Stats() Stats

Stats returns operation statistics.

type Func4

type Func4[A, B, C, D, V any] struct {
	// contains filtered or unexported fields
}

Func4 is a memoized four-argument operation.

func (Func4[A, B, C, D, V]) Call

func (o Func4[A, B, C, D, V]) Call(
	ctx context.Context,
	a A,
	b B,
	c C,
	d D,
) (V, error)

Call returns the memoized result.

func (Func4[A, B, C, D, V]) Clear

func (o Func4[A, B, C, D, V]) Clear()

Clear invalidates every result and in-progress generation.

func (Func4[A, B, C, D, V]) Forget

func (o Func4[A, B, C, D, V]) Forget(a A, b B, c C, d D)

Forget invalidates the result for the arguments.

func (Func4[A, B, C, D, V]) Name

func (o Func4[A, B, C, D, V]) Name() string

Name returns the inferred or configured display name.

func (Func4[A, B, C, D, V]) Refresh

func (o Func4[A, B, C, D, V]) Refresh(
	ctx context.Context,
	a A,
	b B,
	c C,
	d D,
) (V, error)

Refresh reloads the result even if a fresh result exists.

func (Func4[A, B, C, D, V]) Stats

func (o Func4[A, B, C, D, V]) Stats() Stats

Stats returns operation statistics.

type Memoizer

type Memoizer struct {
	// contains filtered or unexported fields
}

Memoizer constructs independently bounded typed operations.

A Memoizer must be constructed with New or MustNew. Constructed values may be copied.

Example
memo := MustNew(Policy{TTL: time.Minute, Capacity: 100})
var calls int
findUser := memo.Func2(func(_ context.Context, tenant, id string) (user, error) {
	calls++
	return user{ID: id, Name: tenant + ":Ada"}, nil
}, Named("users.find"))

first, _ := findUser.Call(context.Background(), "acme", "42")
second, _ := findUser.Call(context.Background(), "acme", "42")
fmt.Println(first.Name, second.Name, calls)
Output:
acme:Ada acme:Ada 1

func MustNew

func MustNew(policy Policy) *Memoizer

MustNew is like New, but panics for an invalid policy.

func New

func New(policy Policy) (*Memoizer, error)

New constructs an empty Memoizer using policy for each operation.

func (*Memoizer) Func

func (m *Memoizer) Func[A comparable, V any](
	load func(context.Context, A) (V, error),
	options ...Option,
) Func[A, V]

Func creates an operation keyed by its argument.

func (*Memoizer) Func2

func (m *Memoizer) Func2[A, B comparable, V any](
	load func(context.Context, A, B) (V, error),
	options ...Option,
) Func2[A, B, V]

Func2 creates an operation keyed by its arguments.

func (*Memoizer) Func2By

func (m *Memoizer) Func2By[A, B any, K comparable, V any](
	load func(context.Context, A, B) (V, error),
	key func(A, B) K,
	options ...Option,
) Func2[A, B, V]

Func2By creates an operation keyed by key.

func (*Memoizer) Func3

func (m *Memoizer) Func3[A, B, C comparable, V any](
	load func(context.Context, A, B, C) (V, error),
	options ...Option,
) Func3[A, B, C, V]

Func3 creates an operation keyed by its arguments.

func (*Memoizer) Func3By

func (m *Memoizer) Func3By[A, B, C any, K comparable, V any](
	load func(context.Context, A, B, C) (V, error),
	key func(A, B, C) K,
	options ...Option,
) Func3[A, B, C, V]

Func3By creates an operation keyed by key.

func (*Memoizer) Func4

func (m *Memoizer) Func4[A, B, C, D comparable, V any](
	load func(context.Context, A, B, C, D) (V, error),
	options ...Option,
) Func4[A, B, C, D, V]

Func4 creates an operation keyed by its arguments.

func (*Memoizer) Func4By

func (m *Memoizer) Func4By[A, B, C, D any, K comparable, V any](
	load func(context.Context, A, B, C, D) (V, error),
	key func(A, B, C, D) K,
	options ...Option,
) Func4[A, B, C, D, V]

Func4By creates an operation keyed by key.

func (*Memoizer) FuncBy

func (m *Memoizer) FuncBy[A any, K comparable, V any](
	load func(context.Context, A) (V, error),
	key func(A) K,
	options ...Option,
) Func[A, V]

FuncBy creates an operation keyed by key rather than directly by its argument.

func (*Memoizer) Once

func (m *Memoizer) Once[V any](
	load func(context.Context) (V, error),
	options ...Option,
) Once[V]

Once creates an operation with one cached result.

type Once

type Once[V any] struct {
	// contains filtered or unexported fields
}

Once is a memoized operation with no ordinary arguments.

func (Once[V]) Call

func (o Once[V]) Call(ctx context.Context) (V, error)

Call returns the memoized result.

func (Once[V]) Clear

func (o Once[V]) Clear()

Clear invalidates every result and in-progress generation.

func (Once[V]) Name

func (o Once[V]) Name() string

Name returns the inferred or configured display name.

func (Once[V]) Refresh

func (o Once[V]) Refresh(ctx context.Context) (V, error)

Refresh reloads the result even if a fresh result exists.

func (Once[V]) Stats

func (o Once[V]) Stats() Stats

Stats returns operation statistics.

type Option

type Option func(*operationOptions)

Option configures one operation.

func CacheErrorsFor

func CacheErrorsFor(duration time.Duration, selected func(error) bool) Option

CacheErrorsFor caches errors accepted by selected for duration.

A zero duration disables negative caching and permits a nil selector.

Context cancellation, dependency-cycle, panic, and Goexit errors are always excluded, regardless of selected.

func Named

func Named(name string) Option

Named overrides the operation name inferred from its loader. An empty name uses the inferred name.

func StaleIfError

func StaleIfError(duration time.Duration) Option

StaleIfError retains successful values for duration after their fresh TTL. A zero duration disables stale fallback.

type PanicError

type PanicError struct {
	Value any
	Stack string
}

PanicError reports a panic recovered from a loader.

func (*PanicError) Error

func (e *PanicError) Error() string

Error implements error.

type Policy

type Policy struct {
	// TTL is the fixed lifetime of a successful value, measured from loader
	// completion. Cache hits do not extend it.
	TTL time.Duration

	// Capacity is the maximum number of completed values retained. In-flight
	// loads do not count toward this limit.
	Capacity int

	// Jitter randomly subtracts up to this duration from TTL.
	Jitter time.Duration
}

Policy controls the default policy of each operation in a Memoizer.

TTL and Capacity must both be positive.

type Stats

type Stats struct {
	// Hits counts Calls served by a fresh completed result.
	Hits uint64
	// Misses counts Calls whose initial lookup found no fresh result.
	Misses uint64
	// Loads counts physical loader invocations.
	Loads uint64
	// Coalesced counts calls attached to an existing in-flight load.
	Coalesced uint64
	// LoadErrors counts physical loads ending in an error, panic, or Goexit.
	LoadErrors uint64
	// StaleHits counts calls that fell back to a stale success.
	StaleHits uint64
	// ErrorHits counts calls served by a negatively cached result.
	ErrorHits uint64
	// Evictions counts completed results removed to enforce Capacity.
	Evictions uint64

	// Entries is the number of completed results currently retained. Because
	// expiration is lazy, it may include expired entries that have not been
	// accessed or displaced.
	Entries int
	// InFlight is the number of loader goroutines that have not returned. It
	// includes detached loads and may exceed one per key.
	InFlight int
}

Stats is a point-in-time snapshot of cache activity.

Counters are cumulative for the cache's lifetime; Clear does not reset them. Entries and InFlight are instantaneous gauges.

Jump to

Keyboard shortcuts

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