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:
- ErrNotFound — key does not exist or has expired
- ErrClosed — operation on a closed cache
- ErrMarshal — value serialization failed
- ErrUnmarshal — value deserialization failed
Use errors.Is to check:
val, err := c.Get(ctx, "key")
if errors.Is(err, cache.ErrNotFound) {
// handle miss
}
Index ¶
- Variables
- func GetOrSet[V any](ctx context.Context, c Cache[V], key string, ...) (V, error)
- type Cache
- type Marshaler
- type Memory
- func (m *Memory[V]) Clear(_ context.Context) error
- func (m *Memory[V]) Close() error
- func (m *Memory[V]) Delete(_ context.Context, key string) error
- func (m *Memory[V]) Get(_ context.Context, key string) (V, error)
- func (m *Memory[V]) Has(_ context.Context, key string) (bool, error)
- func (m *Memory[V]) Set(_ context.Context, key string, value V, ttl time.Duration) error
- func (m *Memory[V]) SetEvictCallback(fn func(key string, value V))
- type MemoryConfig
- type Redis
- func (r *Redis[V]) Clear(ctx context.Context) error
- func (r *Redis[V]) Close() error
- func (r *Redis[V]) Delete(ctx context.Context, key string) error
- func (r *Redis[V]) Get(ctx context.Context, key string) (V, error)
- func (r *Redis[V]) Has(ctx context.Context, key string) (bool, error)
- func (r *Redis[V]) Set(ctx context.Context, key string, value V, ttl time.Duration) error
- type RedisConfig
Constants ¶
This section is empty.
Variables ¶
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 ¶
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]) Close ¶
Close stops the background janitor goroutine and marks the cache as closed. Close is idempotent.
func (*Memory[V]) Get ¶
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]) Set ¶
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 ¶
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 ¶
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 ¶
Close is a no-op for Redis. The Redis client lifecycle is managed separately by the caller (via pkg/redis.Shutdown).
func (*Redis[V]) Get ¶
Get retrieves a value by key from Redis. Returns ErrNotFound if the key does not exist.
type RedisConfig ¶
type RedisConfig struct {
Prefix string `env:"PREFIX"`
DefaultTTL time.Duration `env:"DEFAULT_TTL" envDefault:"5m"`
}
RedisConfig configures the Redis cache.