cache

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Feb 17, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package cache provides a generic Cache interface with in-memory and Redis implementations.

Both implementations share the same Cache interface, making it easy to swap backends or use in-memory caching for development and Redis for production.

Interface

The Cache interface is generic over value type V:

  • Get(ctx, key) (V, error) — retrieve a value
  • Set(ctx, key, value, ttl) error — store a value with TTL
  • Delete(ctx, key) error — remove a key
  • Has(ctx, key) (bool, error) — check existence
  • Clear(ctx) error — remove all entries
  • Close() error — release resources

TTL semantics for Set:

  • Positive duration: item expires after this duration
  • Zero: use the cache's configured default TTL (5 minutes by default)
  • Negative: item never expires

In-Memory Cache

Use NewMemory for single-process applications or testing. It uses a hash map for O(1) lookups and a doubly-linked list for O(1) LRU eviction, with TTL-based expiration via a background janitor goroutine:

c := cache.NewMemory[string](cache.MemoryConfig{
    DefaultTTL:      5 * time.Minute,
    CleanupInterval: 30 * time.Second,
    MaxEntries:      10000,
})
defer c.Close()

c.Set(ctx, "greeting", "hello", 0)   // uses default TTL
val, err := c.Get(ctx, "greeting")   // val = "hello"

Eviction Callbacks

The in-memory cache supports eviction callbacks for resource cleanup:

c := cache.NewMemory[*Connection](cache.MemoryConfig{
    MaxEntries: 100,
})
c.SetEvictCallback(func(key string, conn *Connection) {
    conn.Close()
})

The callback is triggered on LRU eviction, TTL expiration cleanup, manual deletion, and clearing.

Redis Cache

Use NewRedis for distributed caching with a Redis backend. Requires a github.com/redis/go-redis/v9.UniversalClient from github.com/dmitrymomot/forge/pkg/redis:

client := redis.MustOpen(ctx, redis.Config{URL: os.Getenv("REDIS_URL")})
c := cache.NewRedis[User](client, nil, cache.RedisConfig{
    Prefix:     "users",
    DefaultTTL: 30 * time.Minute,
})

c.Set(ctx, "user:123", user, time.Hour)
val, err := c.Get(ctx, "user:123")

Pass a custom Marshaler as the second argument to NewRedis to use a different serialization format (msgpack, protobuf, etc.). If nil, JSON is used.

Cache Stampede Prevention

Use the standalone GetOrSet function to prevent cache stampedes. It uses singleflight to ensure only one goroutine computes a missing value:

val, err := cache.GetOrSet(ctx, c, "user:123", func(ctx context.Context) (User, time.Duration, error) {
    user, err := repo.FindUser(ctx, "123")
    return user, 5 * time.Minute, err
})

Error Handling

The package defines sentinel errors:

Use errors.Is to check:

val, err := c.Get(ctx, "key")
if errors.Is(err, cache.ErrNotFound) {
    // handle miss
}

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound is returned when a key does not exist in the cache or has expired.
	ErrNotFound = errors.New("cache: entry not found")

	// ErrClosed is returned when an operation is attempted on a closed cache.
	ErrClosed = errors.New("cache: closed")

	// ErrMarshal is returned when value serialization fails.
	ErrMarshal = errors.New("cache: failed to marshal value")

	// ErrUnmarshal is returned when value deserialization fails.
	ErrUnmarshal = errors.New("cache: failed to unmarshal value")
)

Sentinel errors for cache operations.

Functions

func GetOrSet

func GetOrSet[V any](ctx context.Context, c Cache[V], key string, fn func(ctx context.Context) (V, time.Duration, error)) (V, error)

GetOrSet retrieves a value from the cache, or calls fn to compute it on a miss. Uses singleflight to prevent cache stampedes: if multiple goroutines call GetOrSet with the same key concurrently, fn is called only once.

The callback returns the value, a TTL for caching, and an error. If fn returns an error, the value is not cached and the error is returned.

Types

type Cache

type Cache[V any] interface {
	// Get retrieves a value by key.
	// Returns ErrNotFound if the key does not exist or has expired.
	Get(ctx context.Context, key string) (V, error)

	// Set stores a value with the given TTL.
	Set(ctx context.Context, key string, value V, ttl time.Duration) error

	// Delete removes a key from the cache.
	Delete(ctx context.Context, key string) error

	// Has checks whether a key exists and has not expired.
	Has(ctx context.Context, key string) (bool, error)

	// Clear removes all entries from the cache.
	Clear(ctx context.Context) error

	// Close releases resources (stops background goroutines, etc.).
	Close() error
}

Cache is a generic key-value cache with TTL support.

TTL semantics for Set:

  • Positive duration: item expires after this duration
  • Zero: use the cache's configured default TTL
  • Negative: item never expires

type Marshaler

type Marshaler[V any] interface {
	Marshal(v V) ([]byte, error)
	Unmarshal(data []byte) (V, error)
}

Marshaler serializes and deserializes cache values for storage backends that require byte representation (e.g., Redis).

type Memory

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

Memory is an in-memory cache with TTL-based expiration and optional LRU eviction when a maximum entry count is configured.

It uses a hash map for O(1) lookups and a doubly-linked list for O(1) LRU eviction ordering. The most recently accessed items are at the front of the list; the least recently used are at the back.

func NewMemory

func NewMemory[V any](cfg MemoryConfig) *Memory[V]

NewMemory creates a new in-memory cache.

Example:

c := cache.NewMemory[string](cache.MemoryConfig{
    DefaultTTL:      5 * time.Minute,
    CleanupInterval: 30 * time.Second,
    MaxEntries:      10000,
})
defer c.Close()

func (*Memory[V]) Clear

func (m *Memory[V]) Clear(_ context.Context) error

Clear removes all entries from the cache.

func (*Memory[V]) Close

func (m *Memory[V]) Close() error

Close stops the background janitor goroutine and marks the cache as closed. Close is idempotent.

func (*Memory[V]) Delete

func (m *Memory[V]) Delete(_ context.Context, key string) error

Delete removes a key from the cache.

func (*Memory[V]) Get

func (m *Memory[V]) Get(_ context.Context, key string) (V, error)

Get retrieves a value by key. Returns ErrNotFound if the key does not exist or has expired. Accessing a key marks it as recently used for LRU purposes.

func (*Memory[V]) Has

func (m *Memory[V]) Has(_ context.Context, key string) (bool, error)

Has checks whether a key exists and has not expired.

func (*Memory[V]) Set

func (m *Memory[V]) Set(_ context.Context, key string, value V, ttl time.Duration) error

Set stores a value with the given TTL. TTL semantics: positive = expires after duration, zero = use default TTL, negative = never expires.

func (*Memory[V]) SetEvictCallback

func (m *Memory[V]) SetEvictCallback(fn func(key string, value V))

SetEvictCallback sets a callback function that is called when items are evicted from the cache. This includes LRU eviction, TTL expiration cleanup, manual deletion, and clearing.

type MemoryConfig

type MemoryConfig struct {
	DefaultTTL      time.Duration `env:"DEFAULT_TTL"      envDefault:"5m"`
	CleanupInterval time.Duration `env:"CLEANUP_INTERVAL" envDefault:"1m"`
	MaxEntries      int           `env:"MAX_ENTRIES"       envDefault:"0"`
}

MemoryConfig configures the in-memory cache.

type Redis

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

Redis is a cache backed by Redis. It serializes values using the configured Marshaler (default: JSON).

func NewRedis

func NewRedis[V any](client redis.UniversalClient, m Marshaler[V], cfg RedisConfig) *Redis[V]

NewRedis creates a new Redis-backed cache. The client should be obtained from pkg/redis.Open or pkg/redis.MustOpen.

An optional Marshaler can be provided to customize serialization. If nil, JSON serialization is used.

Example:

client := redis.MustOpen(ctx, os.Getenv("REDIS_URL"))
c := cache.NewRedis[User](client, nil, cache.RedisConfig{
    Prefix:     "users",
    DefaultTTL: 30 * time.Minute,
})

func (*Redis[V]) Clear

func (r *Redis[V]) Clear(ctx context.Context) error

Clear removes all cache entries. If a prefix is configured, only keys matching the prefix are removed using SCAN. If no prefix is configured, FLUSHDB is used.

func (*Redis[V]) Close

func (r *Redis[V]) Close() error

Close is a no-op for Redis. The Redis client lifecycle is managed separately by the caller (via pkg/redis.Shutdown).

func (*Redis[V]) Delete

func (r *Redis[V]) Delete(ctx context.Context, key string) error

Delete removes a key from Redis.

func (*Redis[V]) Get

func (r *Redis[V]) Get(ctx context.Context, key string) (V, error)

Get retrieves a value by key from Redis. Returns ErrNotFound if the key does not exist.

func (*Redis[V]) Has

func (r *Redis[V]) Has(ctx context.Context, key string) (bool, error)

Has checks whether a key exists in Redis.

func (*Redis[V]) Set

func (r *Redis[V]) Set(ctx context.Context, key string, value V, ttl time.Duration) error

Set stores a value in Redis with the given TTL. TTL semantics: positive = expires after duration, zero = use default TTL, negative = no expiration (persists until manually deleted or Redis evicts it).

type RedisConfig

type RedisConfig struct {
	Prefix     string        `env:"PREFIX"`
	DefaultTTL time.Duration `env:"DEFAULT_TTL" envDefault:"5m"`
}

RedisConfig configures the Redis cache.

Jump to

Keyboard shortcuts

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