cache

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Apr 29, 2026 License: MIT Imports: 3 Imported by: 0

README

go-cache

CI Go Reference Go Report Card

Generic, thread-safe in-memory caches for Go with first-class support for negative caching (remembering what's not in your source) and multi-index lookups (one value, many keys).

go get github.com/troybleiben/go-cache

Requires Go 1.21+.

Why

Most cache libraries answer "is the value here?" with yes or no. That collapses two very different states:

  • Miss — we have never asked the source about this key.
  • Not found — we asked, the source had nothing, and asking again right now is wasted work.

go-cache exposes both, so you can skip database round-trips for keys you already know don't exist. It also handles the common case where one record has multiple unique identifiers (an internal ID and an external code, for example) without forcing you to maintain parallel caches by hand.

Features

  • Generic. Memory[K, V] and MemoryMultiCache[V] work with any types.
  • Negative caching. MarkNotFound records confirmed-missing keys with a separate TTL. Re-checks happen at your chosen cadence, not on every request.
  • Three-state lookup. StatusHit / StatusNotFound / StatusMiss.
  • Multi-index. Look up the same value by ID, code, email, whatever. Updates and deletes stay consistent across all indexes automatically.
  • Per-key TTL overrides. SetWithTTL / MarkNotFoundWithTTL for the rare entry that needs different timing than the default.
  • Background janitor. Periodic eviction of expired entries, plus lazy cleanup on read.
  • Thread-safe. All operations are safe for concurrent use; the test suite runs under -race.
  • Zero dependencies. Standard library only.

Quick start

Single-key cache
package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/troybleiben/go-cache"
)

type User struct{ ID, Name string }

var errNotFound = errors.New("not found")

func main() {
	users := cache.NewMemory[string, *User](
		10*time.Minute, // positive TTL
		1*time.Minute,  // negative TTL
	)
	defer users.Close()

	get := func(id string) (*User, error) {
		switch v, st := users.Lookup(id); st {
		case cache.StatusHit:
			return v, nil
		case cache.StatusNotFound:
			return nil, errNotFound
		}
		u, err := loadFromDB(id) // your DB call
		if errors.Is(err, errNotFound) {
			users.MarkNotFound(id)
			return nil, err
		}
		if err != nil {
			return nil, err
		}
		users.Set(id, u)
		return u, nil
	}

	u, err := get("u1")
	fmt.Println(u, err)
}
Multi-index cache

When the same record has multiple unique identifiers — for example a UUID and a human-readable code — register one extractor per index:

type Product struct {
ID   string
SKU  string
Name string
}

products := cache.NewMemoryMultiCache[*Product](
10*time.Minute,
1*time.Minute,
map[string]cache.IndexFunc[*Product]{
"id":  func(p *Product) (any, bool) { return p.ID, p.ID != "" },
"sku": func(p *Product) (any, bool) { return p.SKU, p.SKU != "" },
},
)
defer products.Close()

products.Set(&Product{ID: "p1", SKU: "SKU-001", Name: "Widget"})

p, _      := products.Get("id", "p1")
p, _       = products.Get("sku", "SKU-001")

// Update p1 with a new SKU. The old SKU entry is removed automatically.
products.Set(&Product{ID: "p1", SKU: "SKU-001-V2", Name: "Widget v2"})

_, ok := products.Get("sku", "SKU-001") // ok == false

API overview

Memory[K, V]
Method Purpose
NewMemory[K, V](ttl, negTTL) Construct a new cache.
Set(k, v) / SetWithTTL(k, v, ttl) Store a value.
MarkNotFound(k) / MarkNotFoundWithTTL(k, ttl) Record a confirmed-missing key.
Lookup(k) (V, Status) Three-state lookup.
Get(k) (V, bool) Two-state convenience form.
Delete(k) Remove from both positive and negative tables.
Keys() / Values() / Items() Snapshot of valid positive entries.
NotFoundKeys() Snapshot of valid negative entries.
Len() / NotFoundLen() Counts.
Clear() / ClearNotFound() Bulk removal.
Close() Stop the janitor.
MemoryMultiCache[V]
Method Purpose
NewMemoryMultiCache[V](ttl, negTTL, extractors) Construct with one or more named index extractors.
Set(v) / SetWithTTL(v, ttl) Store under every index whose extractor returns ok=true.
MarkNotFound(idx, key) / MarkNotFoundWithTTL(...) Negative entry on a specific index.
Lookup(idx, key) (V, Status, error) Three-state lookup; ErrUnknownIndex if idx isn't registered.
Get(idx, key) (V, bool) Convenience form.
Delete(idx, key) Remove the value from every index it occupies.
DeleteValue(v) Same, but you supply the value directly.
Values() / Keys(idx) Snapshots.
IndexNames() List registered indexes.
Len() / NotFoundLen(idx) Counts.
Clear() / ClearNotFound(idx) Bulk removal; pass "" to clear negatives across all indexes.
Close() Stop the janitor.

Behavior notes

  • TTL of 0 means no expiry. If both TTLs are 0, no janitor goroutine runs; cleanup happens lazily on read.
  • Setting clears matching not-found. A Set removes any negative entry on the same key, so freshly-inserted source data becomes visible immediately.
  • MarkNotFound removes a positive entry. Mutually exclusive states.
  • Multi-index updates are atomic. Replacing a value with one that has a different key on some index drops the old key — no stale reads.
  • Negative entries are per-index in MemoryMultiCache. Marking a SKU as not-found does not affect lookups by ID.
  • Pointer values are shared, not copied. Mutating a value retrieved from the cache mutates the cached object. The cache mutex protects only its own maps.

A good default is negative TTL shorter than positive TTL: stale "not found" results are usually worse than stale values, because newly-inserted source rows should become visible quickly. 10m positive, 1m negative is a reasonable starting point; tune based on how often your source gains new rows.

Examples

Runnable examples live in examples/:

go run ./examples/basic
go run ./examples/multiindex

Development

This project ships with a Makefile that wraps the common workflows. Run make help to see all targets.

Common tasks
make check           # tidy + fmt + vet + lint + test (run before every PR)
make test            # tests with -race
make cover           # tests with coverage; prints total
make cover-html      # open coverage.html in your browser
make bench           # run benchmarks (Benchmark* funcs)
make examples        # build and run all examples
make install-tools   # install golangci-lint
Releasing

The tag-* targets compute the next semver tag from the latest one and push it. The working tree must be clean. Use DRY_RUN=1 to preview without creating the tag.

make current-version             # show the latest tag (e.g. v0.1.0)
make tag-patch                   # v0.1.0 -> v0.1.1   (bug fixes)
make tag-minor                   # v0.1.3 -> v0.2.0   (new features, backward compatible)
make tag-major                   # v0.4.2 -> v1.0.0   (breaking changes)
make tag VERSION=v0.5.0-rc.1     # explicit version (e.g. pre-releases)
make tag-minor DRY_RUN=1         # preview without tagging

The Makefile validates the resulting version against semver, refuses to overwrite existing tags, and warns if you tag from a non-default branch. After tagging, pkg.go.dev picks up the new version within a few minutes.

Don't forget to update CHANGELOG.md before tagging.

Versioning

This project follows Semantic Versioning. Until v1.0.0 the API may change between minor versions; we will note breaking changes in CHANGELOG.md.

License

MIT

Documentation

Overview

Package cache provides generic, thread-safe in-memory caches with first-class support for negative (not-found) entries and per-key TTLs.

Two cache types are exposed:

  • Memory[K, V] is a single-key cache that maps a comparable key K to a value V. It keeps positive (value) and negative (not-found) entries in separate tables, so callers can distinguish "we never asked" from "we asked and the source had nothing".

  • MemoryMultiCache[V] stores a single value V that can be looked up via multiple independent indexes (for example, by ID and by Code). Indexes are kept consistent automatically: updating a value rewrites all index entries it touches, deleting via one index removes the value from every index it occupies, and each index has its own negative table.

Both caches run an optional background janitor that removes expired entries. Call Close when done to stop it.

Lookup states

Lookups return one of three states:

StatusHit       value is cached and valid; use it
StatusNotFound  source was checked previously and had no such key
StatusMiss      cache has no information; query the source

The classic two-value Get method collapses Miss and NotFound to (zero, false). Use Lookup if you need to tell them apart, e.g. to skip a database round-trip.

Value semantics

V is stored as-is. Pointer types are shared; struct values are copied at Set and at Get time. Concurrent mutation of pointer values is the caller's responsibility — the cache's mutex protects only its internal maps.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnknownIndex = errors.New("cache: unknown index")

ErrUnknownIndex is returned when an index name is used that was not registered at construction time.

Functions

This section is empty.

Types

type IndexFunc

type IndexFunc[V any] func(V) (key any, ok bool)

IndexFunc derives an index key from a value. Return ok=false to skip this index for the given value (e.g. an optional field is empty); the value is still stored, just not reachable through this index.

type Memory

type Memory[K comparable, V any] struct {
	// contains filtered or unexported fields
}

Memory is a generic, thread-safe in-memory cache that keeps positive (value) and negative (not-found) entries in separate tables.

func NewMemory

func NewMemory[K comparable, V any](ttl, negTTL time.Duration) *Memory[K, V]

NewMemory creates a new cache with the given TTLs.

  • ttl: lifetime of positive entries. 0 disables expiry.
  • negTTL: lifetime of negative entries. 0 disables expiry.

A background janitor goroutine runs if at least one TTL is > 0. Call Close() when done.

func (*Memory[K, V]) Clear

func (c *Memory[K, V]) Clear()

Clear removes all entries (positive and negative).

func (*Memory[K, V]) ClearNotFound

func (c *Memory[K, V]) ClearNotFound()

ClearNotFound removes only the negative entries. Useful after a bulk insert into the underlying source, when negative entries should be re-evaluated.

func (*Memory[K, V]) Close

func (c *Memory[K, V]) Close()

Close stops the janitor goroutine. Safe to call multiple times.

func (*Memory[K, V]) Delete

func (c *Memory[K, V]) Delete(key K)

Delete removes the key from both positive and negative tables.

func (*Memory[K, V]) Get

func (c *Memory[K, V]) Get(key K) (V, bool)

Get is a classic two-value cache API. It returns (value, true) only on StatusHit; both Miss and NotFound collapse to (zero, false). Use Lookup when you need to distinguish those two cases.

func (*Memory[K, V]) Items

func (c *Memory[K, V]) Items() map[K]V

Items returns a snapshot map of all valid positive entries.

func (*Memory[K, V]) Keys

func (c *Memory[K, V]) Keys() []K

Keys returns the keys of all valid positive entries.

func (*Memory[K, V]) Len

func (c *Memory[K, V]) Len() int

Len returns the number of valid (non-expired) positive entries.

func (*Memory[K, V]) Lookup

func (c *Memory[K, V]) Lookup(key K) (V, Status)

Lookup returns the cached state for the key. Three outcomes:

  • StatusHit: value is returned.
  • StatusNotFound: key is known to be missing in the source.
  • StatusMiss: nothing is known; query the source.

For non-Hit outcomes, value is the zero value.

func (*Memory[K, V]) MarkNotFound

func (c *Memory[K, V]) MarkNotFound(key K)

MarkNotFound records the key as confirmed-missing using the default negative TTL. Any positive entry for the same key is removed.

func (*Memory[K, V]) MarkNotFoundWithTTL

func (c *Memory[K, V]) MarkNotFoundWithTTL(key K, ttl time.Duration)

MarkNotFoundWithTTL records a not-found entry with the given TTL. ttl=0 means no expiry.

func (*Memory[K, V]) NotFoundKeys

func (c *Memory[K, V]) NotFoundKeys() []K

NotFoundKeys returns the keys of all valid negative entries.

func (*Memory[K, V]) NotFoundLen

func (c *Memory[K, V]) NotFoundLen() int

NotFoundLen returns the number of valid negative entries.

func (*Memory[K, V]) Set

func (c *Memory[K, V]) Set(key K, value V)

Set stores the value with the default TTL. Any existing not-found entry for the same key is removed.

func (*Memory[K, V]) SetWithTTL

func (c *Memory[K, V]) SetWithTTL(key K, value V, ttl time.Duration)

SetWithTTL stores the value with the given TTL. ttl=0 means no expiry.

func (*Memory[K, V]) Values

func (c *Memory[K, V]) Values() []V

Values returns the values of all valid positive entries.

type MemoryMultiCache

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

MemoryMultiCache stores values that can be looked up through multiple independent indexes (for example: by ID and by Code). It is generic over the value type V; index keys flow through `any` because each index may use a different key type.

Consistency guarantees:

  • Set updates all indexes atomically. If a new value collides with an existing entry on any index, the old entry's other index keys are removed too — there are no stale entries.
  • Delete removes the value from every index it is registered under.
  • Each index maintains its own not-found set.

func NewMemoryMultiCache

func NewMemoryMultiCache[V any](
	ttl, negTTL time.Duration,
	extractors map[string]IndexFunc[V],
) *MemoryMultiCache[V]

NewMemoryMultiCache creates a new multi-index cache.

  • ttl: default TTL for positive entries (0 = no expiry)
  • negTTL: default TTL for negative entries (0 = no expiry)
  • extractors: map of index name to key extractor; at least one required.

func (*MemoryMultiCache[V]) Clear

func (mc *MemoryMultiCache[V]) Clear()

Clear removes everything (positive and negative across all indexes).

func (*MemoryMultiCache[V]) ClearNotFound

func (mc *MemoryMultiCache[V]) ClearNotFound(indexName string) error

ClearNotFound removes negative entries on the given index. Pass an empty string to clear negatives across all indexes.

func (*MemoryMultiCache[V]) Close

func (mc *MemoryMultiCache[V]) Close()

Close stops the janitor goroutine. Safe to call multiple times.

func (*MemoryMultiCache[V]) Delete

func (mc *MemoryMultiCache[V]) Delete(indexName string, key any) (bool, error)

Delete removes the value reachable via (indexName, key) from every index it occupies. Returns true if a value was removed.

func (*MemoryMultiCache[V]) DeleteValue

func (mc *MemoryMultiCache[V]) DeleteValue(value V) bool

DeleteValue removes the value from every index it occupies, by recomputing its index keys with the registered extractors. Useful when the caller has the value object directly.

func (*MemoryMultiCache[V]) Get

func (mc *MemoryMultiCache[V]) Get(indexName string, key any) (V, bool)

Get is the two-value convenience form of Lookup. Returns (zero, false) for both Miss and NotFound. Returns (zero, false) and silently ignores unknown index names — use Lookup if you need to detect that.

func (*MemoryMultiCache[V]) IndexNames

func (mc *MemoryMultiCache[V]) IndexNames() []string

IndexNames returns the registered index names.

func (*MemoryMultiCache[V]) Keys

func (mc *MemoryMultiCache[V]) Keys(indexName string) ([]any, error)

Keys returns the keys present on the given index, for valid entries only.

func (*MemoryMultiCache[V]) Len

func (mc *MemoryMultiCache[V]) Len() int

Len returns the number of valid (non-expired) values stored.

func (*MemoryMultiCache[V]) Lookup

func (mc *MemoryMultiCache[V]) Lookup(indexName string, key any) (V, Status, error)

Lookup returns the cached state for (indexName, key).

func (*MemoryMultiCache[V]) MarkNotFound

func (mc *MemoryMultiCache[V]) MarkNotFound(indexName string, key any) error

MarkNotFound records a not-found entry on the given index using the default negative TTL. If a positive entry exists at that index key, it is removed (along with its other index entries).

func (*MemoryMultiCache[V]) MarkNotFoundWithTTL

func (mc *MemoryMultiCache[V]) MarkNotFoundWithTTL(indexName string, key any, ttl time.Duration) error

MarkNotFoundWithTTL is like MarkNotFound with a per-call TTL override.

func (*MemoryMultiCache[V]) NotFoundLen

func (mc *MemoryMultiCache[V]) NotFoundLen(indexName string) (int, error)

NotFoundLen returns the number of valid negative entries on the given index.

func (*MemoryMultiCache[V]) Set

func (mc *MemoryMultiCache[V]) Set(value V)

Set stores the value, registering it under every index whose extractor returns ok=true. Existing entries colliding on any index are fully removed (from all their indexes) before the new value is inserted.

func (*MemoryMultiCache[V]) SetWithTTL

func (mc *MemoryMultiCache[V]) SetWithTTL(value V, ttl time.Duration)

SetWithTTL is like Set but with a per-call TTL override. ttl=0 = no expiry.

func (*MemoryMultiCache[V]) Values

func (mc *MemoryMultiCache[V]) Values() []V

Values returns a snapshot of all valid values.

type Status

type Status int

Status represents the result of a cache lookup.

const (
	// StatusMiss: the cache has no information about the key. The caller
	// should query the underlying source (e.g. database).
	StatusMiss Status = iota
	// StatusHit: the key exists and a valid value is returned.
	StatusHit
	// StatusNotFound: a previous lookup confirmed the key does not exist
	// in the underlying source. Caller should not query again until the
	// negative entry expires.
	StatusNotFound
)

func (Status) String

func (s Status) String() string

Directories

Path Synopsis
examples
basic command
multiindex command

Jump to

Keyboard shortcuts

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