cache

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 6, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

cache-kit

Go Reference Go Report Card License codecov

中文文档

A Go library for thread-safe, multi-index memory caching with Redis support.

Features

  • Multi-Index Lookup: O(1) lookups by multiple keys (e.g., ID, email, phone)
  • Thread-Safe: Safe for concurrent read/write access
  • Hash-Based Change Detection: Detect cache changes efficiently
  • Redis Support: Redis cache adapter for distributed scenarios
  • Hybrid Cache: Combine memory and Redis for optimal performance
  • Generic Types: Works with any data type using Go generics
  • Fluent Configuration: Builder pattern for easy configuration

Installation

go get github.com/soulteary/cache-kit

Quick Start

Memory Cache with Multi-Index
package main

import (
    "fmt"
    cache "github.com/soulteary/cache-kit"
)

type User struct {
    ID    string
    Email string
    Phone string
    Name  string
}

func main() {
    // Create cache with primary key
    config := cache.DefaultConfig[User]().
        WithPrimaryKey(func(u User) string { return u.ID })

    c := cache.NewMultiIndexCache(config)

    // Add indexes for fast lookup by email and phone
    c.AddIndex("email", func(u User) string { return u.Email })
    c.AddIndex("phone", func(u User) string { return u.Phone })

    // Set data
    users := []User{
        {ID: "1", Email: "alice@example.com", Phone: "1111111111", Name: "Alice"},
        {ID: "2", Email: "bob@example.com", Phone: "2222222222", Name: "Bob"},
    }
    c.Set(users)

    // Lookup by primary key (O(1))
    user, ok := c.Get("1")
    if ok {
        fmt.Println("Found by ID:", user.Name)
    }

    // Lookup by email index (O(1))
    user, ok = c.GetByIndex("email", "bob@example.com")
    if ok {
        fmt.Println("Found by email:", user.Name)
    }

    // Lookup by phone index (O(1))
    user, ok = c.GetByIndex("phone", "1111111111")
    if ok {
        fmt.Println("Found by phone:", user.Name)
    }

    // Get hash for change detection
    fmt.Println("Cache hash:", c.GetHash())
}
Redis Cache
package main

import (
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
    cache "github.com/soulteary/cache-kit"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    // Create Redis cache
    config := cache.DefaultRedisConfig().
        WithKeyPrefix("myapp:users:").
        WithTTL(30 * time.Minute)

    c := cache.NewRedisCache[User](client, config)

    // Set data
    users := []User{{ID: "1", Name: "Alice"}}
    if err := c.Set(users); err != nil {
        panic(err)
    }

    // Get data
    users, err := c.Get()
    if err != nil {
        panic(err)
    }

    // Check version (for cache invalidation)
    version, _ := c.GetVersion()
    fmt.Println("Cache version:", version)
}
Hybrid Cache (Memory + Redis)
package main

import (
    "github.com/redis/go-redis/v9"
    cache "github.com/soulteary/cache-kit"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    // Create hybrid cache
    memConfig := cache.DefaultConfig[User]().
        WithPrimaryKey(func(u User) string { return u.ID })
    redisConfig := cache.DefaultRedisConfig().WithKeyPrefix("users:")

    c := cache.NewHybridCache[User](memConfig, client, redisConfig)

    // Add indexes
    c.AddIndex("email", func(u User) string { return u.Email })

    // Set stores in both memory and Redis
    if err := c.Set([]User{{ID: "1", Email: "alice@example.com"}}); err != nil {
        panic(err)
    }

    // Fast lookup from memory
    user, ok := c.GetByIndex("email", "alice@example.com")

    // Load from Redis on startup
    c.LoadFromRedis()

    // Sync memory to Redis
    c.SyncToRedis()
}

Configuration

Memory Cache Config

PrimaryKeyFunc is required for MultiIndexCache when storing non-empty data; if not set, Set(non-empty) panics. Set(empty slice) is allowed without PrimaryKeyFunc.

config := cache.DefaultConfig[User]().
    // Required: Primary key extraction
    WithPrimaryKey(func(u User) string { return u.ID }).

    // Optional: Custom hash function
    WithHashFunc(func(users []User) string {
        // Your custom hash logic
        return "custom-hash"
    }).

    // Optional: Validation (skip invalid items)
    WithValidateFunc(func(u User) error {
        if u.ID == "" {
            return fmt.Errorf("ID required")
        }
        return nil
    }).

    // Optional: Normalization (transform before storing)
    WithNormalizeFunc(func(u User) User {
        u.Email = strings.ToLower(u.Email)
        return u
    }).

    // Optional: Sort function for deterministic hashing
    WithSortFunc(cache.StringSorter(func(u User) string {
        return u.ID
    }))
Redis Config
  • KeyPrefix and VersionKeySuffix must be non-empty. NewRedisCacheWithKey requires a non-empty key. Use a unique prefix or key per cache to avoid key collision and key space pollution.
  • Key length (data key and version key) must not exceed 512 bytes.
  • Actual Redis keys: data key = KeyPrefix + "data" (e.g. myapp:cache:data); version key = data key + VersionKeySuffix (e.g. myapp:cache:data:version).
  • DefaultRedisConfig default values: KeyPrefix is "cache:", VersionKeySuffix is ":version"; override with a unique prefix per cache.
config := cache.DefaultRedisConfig().
    WithKeyPrefix("myapp:cache:").    // Key prefix (required, non-empty; default "cache:"; use unique prefix per cache)
    WithVersionKeySuffix(":version"). // Version key suffix (required, non-empty; default ":version")
    WithTTL(1 * time.Hour).           // Cache TTL (must be positive; 0 means use default 1h at Set time)
    WithOperationTimeout(5 * time.Second). // Operation timeout
    WithMaxValueBytes(4 * 1024 * 1024) // Optional: max value size for Get() to prevent OOM (default 16MB)

Hash / change detection: The default hash function uses fmt.Sprintf("%v", v) for each value. Do not use it for types containing sensitive fields (passwords, tokens). For structs with maps or pointer fields, the default hash may be non-deterministic; use WithHashFunc with a custom implementation that hashes only stable, non-sensitive fields in a fixed order.

HybridCache.Set: Memory is updated first, then Redis. If Redis fails, memory already has the new data; handle the error (e.g. retry or call LoadFromRedis) to reconcile.

API Reference

MemoryCache
// Create cache
cache := NewMultiIndexCache[V](config)

// Index management
cache.AddIndex(name, keyFunc)
cache.RemoveIndex(name)
cache.HasIndex(name) bool
cache.IndexCount() int
cache.IndexNames() []string

// Data operations
cache.Set(values)
cache.Get(primaryKey) (V, bool)
cache.GetByIndex(indexName, key) (V, bool)
cache.GetAll() []V
cache.Len() int
cache.Clear()

// Iteration (callback must not panic)
cache.Iterate(func(v V) bool)

// Change detection
cache.GetHash() string
RedisCache
// Create cache
cache := NewRedisCache[V](client, config)
cache := NewRedisCacheWithKey[V](client, "custom:key", config)

// Data operations
cache.Set(values) error
cache.SetWithTTL(values, ttl) error
cache.Get() ([]V, error)
cache.Clear() error   // Also removes version key; after Clear(), GetVersion() returns 0

// Status
cache.Exists() (bool, error)
cache.GetVersion() (int64, error)
cache.TTL() (time.Duration, error)
cache.Refresh() error
HybridCache
// Create cache
cache := NewHybridCache[V](memConfig, redisClient, redisConfig)

// Index management
cache.AddIndex(name, keyFunc)

// Data operations
cache.Set(values) error
cache.GetByIndex(indexName, key) (V, bool)
cache.GetAll() []V

// Sync operations
cache.LoadFromRedis() error
cache.SyncToRedis() error

// Access underlying caches
cache.Memory() *MemoryCache[V]
cache.Redis() *RedisCache[V]

Use Cases

  • User whitelist caching: Fast O(1) lookups by phone, email, or user ID
  • Configuration caching: Cache config with multi-key access
  • Hot data caching: Frequently accessed data with multiple indexes
  • Distributed caching: Share cache across instances with Redis

License

Apache License 2.0

Documentation

Overview

Package cache provides multi-index memory cache with Redis support.

This package offers a generic, thread-safe caching solution with the following features:

  • Multi-index lookup support (O(1) lookups by different keys)
  • Hash-based change detection
  • Redis cache adapter for distributed scenarios
  • Automatic TTL management

Example usage:

config := cache.DefaultConfig[User]().WithPrimaryKey(func(u User) string { return u.ID })
cache := cache.NewMultiIndexCache[User](config)
cache.AddIndex("email", func(u User) string { return u.Email })
cache.Set(users)
user, ok := cache.GetByIndex("email", "user@example.com")

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StringSorter

func StringSorter[V any](keyFunc func(V) string) func([]V) []V

StringSorter provides a helper for sorting slices by a string key.

Types

type Cache

type Cache[K comparable, V any] interface {
	// Get retrieves a value by its primary key.
	Get(key K) (V, bool)

	// Set stores a value with its primary key.
	Set(key K, value V)

	// Delete removes a value by its primary key.
	Delete(key K)

	// GetAll returns all cached values.
	GetAll() []V

	// Len returns the number of cached items.
	Len() int

	// Clear removes all items from the cache.
	Clear()

	// GetHash returns a hash representing the current cache state.
	// Useful for detecting changes.
	GetHash() string
}

Cache provides the basic cache interface.

type Config

type Config[V any] struct {
	// PrimaryKeyFunc extracts the primary key from a value.
	// This is required for multi-index cache.
	PrimaryKeyFunc KeyFunc[V]

	// HashFunc computes a hash for the cache contents.
	// If nil, defaultHashFunc is used, which serializes each value with fmt.Sprintf("%v", v).
	// Warning: the default is not suitable for types containing sensitive fields (passwords, tokens),
	// as those would be included in the hash input. It may also be non-deterministic for types
	// with maps or pointer fields. Use WithHashFunc to supply a custom hash (e.g. only stable,
	// non-sensitive fields in a deterministic order).
	HashFunc HashFunc[V]

	// ValidateFunc validates a value before storing.
	// If nil, all values are accepted.
	ValidateFunc ValidateFunc[V]

	// NormalizeFunc normalizes a value before storing.
	// If nil, values are stored as-is.
	NormalizeFunc NormalizeFunc[V]

	// SortFunc is used for deterministic hash calculation.
	// If nil, values are hashed in insertion order.
	SortFunc func(values []V) []V
}

Config holds configuration for the cache.

func DefaultConfig

func DefaultConfig[V any]() *Config[V]

DefaultConfig returns a default configuration. Note: PrimaryKeyFunc must be set before use with MultiIndexCache.

func (*Config[V]) WithHashFunc

func (c *Config[V]) WithHashFunc(fn HashFunc[V]) *Config[V]

WithHashFunc sets a custom hash function.

func (*Config[V]) WithNormalizeFunc

func (c *Config[V]) WithNormalizeFunc(fn NormalizeFunc[V]) *Config[V]

WithNormalizeFunc sets a normalization function.

func (*Config[V]) WithPrimaryKey

func (c *Config[V]) WithPrimaryKey(fn KeyFunc[V]) *Config[V]

WithPrimaryKey sets the primary key extraction function.

func (*Config[V]) WithSortFunc

func (c *Config[V]) WithSortFunc(fn func(values []V) []V) *Config[V]

WithSortFunc sets a sort function for deterministic hashing.

func (*Config[V]) WithValidateFunc

func (c *Config[V]) WithValidateFunc(fn ValidateFunc[V]) *Config[V]

WithValidateFunc sets a validation function.

type HashFunc

type HashFunc[V any] func(values []V) string

HashFunc defines a function that computes a hash for a value.

type HybridCache

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

HybridCache combines memory cache with Redis for distributed scenarios. It uses memory cache for fast local access and Redis for persistence/sharing.

func NewHybridCache

func NewHybridCache[V any](memoryConfig *Config[V], redisClient *redis.Client, redisConfig *RedisConfig) *HybridCache[V]

NewHybridCache creates a new hybrid cache.

func (*HybridCache[V]) AddIndex

func (c *HybridCache[V]) AddIndex(name string, keyFunc KeyFunc[V])

AddIndex registers a new index on the memory cache.

func (*HybridCache[V]) GetAll

func (c *HybridCache[V]) GetAll() []V

GetAll returns all values from memory cache.

func (*HybridCache[V]) GetByIndex

func (c *HybridCache[V]) GetByIndex(indexName string, key string) (V, bool)

GetByIndex retrieves a value from memory cache by index.

func (*HybridCache[V]) LoadFromRedis

func (c *HybridCache[V]) LoadFromRedis() error

LoadFromRedis loads data from Redis into memory cache.

func (*HybridCache[V]) Memory

func (c *HybridCache[V]) Memory() *MemoryCache[V]

Memory returns the underlying memory cache for direct access.

func (*HybridCache[V]) Redis

func (c *HybridCache[V]) Redis() *RedisCache[V]

Redis returns the underlying Redis cache for direct access.

func (*HybridCache[V]) Set

func (c *HybridCache[V]) Set(values []V) error

Set stores values in both memory and Redis. Memory is updated first, then Redis. If Redis.Set fails, memory already holds the new data while Redis may still have the old data; the error is returned and the caller should retry or call LoadFromRedis to reconcile (e.g. clear memory or reload from Redis).

func (*HybridCache[V]) SyncToRedis

func (c *HybridCache[V]) SyncToRedis() error

SyncToRedis saves memory cache data to Redis.

type KeyFunc

type KeyFunc[V any] func(value V) string

KeyFunc defines a function that extracts a key from a value.

type MemoryCache

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

MemoryCache provides a thread-safe, multi-index memory cache.

It supports O(1) lookups by primary key and by any registered index. The cache maintains insertion order and provides hash-based change detection.

func NewMultiIndexCache

func NewMultiIndexCache[V any](config *Config[V]) *MemoryCache[V]

NewMultiIndexCache creates a new multi-index memory cache. The config must have a PrimaryKeyFunc set.

func (*MemoryCache[V]) AddIndex

func (c *MemoryCache[V]) AddIndex(name string, keyFunc KeyFunc[V])

AddIndex registers a new index with a key extraction function. The keyFunc extracts the index key from a value. If an index with the same name exists, it will be replaced.

func (*MemoryCache[V]) Clear

func (c *MemoryCache[V]) Clear()

Clear removes all items from the cache.

func (*MemoryCache[V]) Get

func (c *MemoryCache[V]) Get(key string) (V, bool)

Get retrieves a value by its primary key.

func (*MemoryCache[V]) GetAll

func (c *MemoryCache[V]) GetAll() []V

GetAll returns all cached values in insertion order.

func (*MemoryCache[V]) GetByIndex

func (c *MemoryCache[V]) GetByIndex(indexName string, key string) (V, bool)

GetByIndex retrieves a value by a named index. Returns the value and true if found, zero value and false otherwise.

func (*MemoryCache[V]) GetHash

func (c *MemoryCache[V]) GetHash() string

GetHash returns a hash representing the current cache state.

func (*MemoryCache[V]) HasIndex

func (c *MemoryCache[V]) HasIndex(name string) bool

HasIndex checks if an index exists.

func (*MemoryCache[V]) IndexCount

func (c *MemoryCache[V]) IndexCount() int

IndexCount returns the number of registered indexes.

func (*MemoryCache[V]) IndexNames

func (c *MemoryCache[V]) IndexNames() []string

IndexNames returns the names of all registered indexes.

func (*MemoryCache[V]) Iterate

func (c *MemoryCache[V]) Iterate(fn func(value V) bool)

Iterate applies a function to each cached value in insertion order. If the function returns false, iteration stops. The callback must not panic; if it does, the read lock may block other goroutines until recovery.

func (*MemoryCache[V]) Len

func (c *MemoryCache[V]) Len() int

Len returns the number of cached items.

func (*MemoryCache[V]) RemoveIndex

func (c *MemoryCache[V]) RemoveIndex(name string)

RemoveIndex removes an index by name.

func (*MemoryCache[V]) Set

func (c *MemoryCache[V]) Set(values []V)

Set stores all values and rebuilds all indexes. Values are validated and normalized if the corresponding functions are set. Duplicate primary keys will be updated (last one wins). Panics if PrimaryKeyFunc is nil and len(values) > 0; set PrimaryKeyFunc via config before use with non-empty data.

type MultiIndexCache

type MultiIndexCache[V any] interface {
	// GetByIndex retrieves a value by a named index.
	// Returns the value and true if found, zero value and false otherwise.
	GetByIndex(indexName string, key string) (V, bool)

	// AddIndex registers a new index with a key extraction function.
	// The keyFunc extracts the index key from a value.
	AddIndex(name string, keyFunc func(V) string)

	// RemoveIndex removes an index by name.
	RemoveIndex(name string)

	// HasIndex checks if an index exists.
	HasIndex(name string) bool

	// Set stores all values and rebuilds all indexes.
	Set(values []V)

	// GetAll returns all cached values in insertion order.
	GetAll() []V

	// Len returns the number of cached items.
	Len() int

	// Clear removes all items from the cache.
	Clear()

	// GetHash returns a hash representing the current cache state.
	GetHash() string

	// Iterate applies a function to each cached value.
	// If the function returns false, iteration stops.
	Iterate(fn func(value V) bool)
}

MultiIndexCache extends Cache with multi-index lookup capabilities.

type NormalizeFunc

type NormalizeFunc[V any] func(value V) V

NormalizeFunc defines a function that normalizes a value. Returns the normalized value.

type RedisCache

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

RedisCache provides a Redis-based cache implementation. It supports versioning for cache invalidation detection.

func NewRedisCache

func NewRedisCache[V any](client *redis.Client, config *RedisConfig) *RedisCache[V]

NewRedisCache creates a new Redis cache with the given client and configuration. KeyPrefix and VersionKeySuffix must be non-empty; use a unique prefix per cache to avoid key collision.

func NewRedisCacheWithKey

func NewRedisCacheWithKey[V any](client *redis.Client, key string, config *RedisConfig) *RedisCache[V]

NewRedisCacheWithKey creates a new Redis cache with a custom key name. The key must be non-empty; VersionKeySuffix must be non-empty. Use a unique key per cache to avoid key collision.

func (*RedisCache[V]) Clear

func (c *RedisCache[V]) Clear() error

Clear deletes the cache key and the version key. After Clear(), GetVersion() returns 0 (version key is removed).

func (*RedisCache[V]) Exists

func (c *RedisCache[V]) Exists() (bool, error)

Exists checks if the cache key exists.

func (*RedisCache[V]) Get

func (c *RedisCache[V]) Get() ([]V, error)

Get retrieves values from Redis. Returns an empty slice if the key doesn't exist. If the stored value exceeds MaxValueBytes (when set in config), returns an error to prevent OOM.

func (*RedisCache[V]) GetVersion

func (c *RedisCache[V]) GetVersion() (int64, error)

GetVersion returns the current cache version. Returns 0 if the version key doesn't exist.

func (*RedisCache[V]) Refresh

func (c *RedisCache[V]) Refresh() error

Refresh extends the TTL of the cache without changing the data.

func (*RedisCache[V]) Set

func (c *RedisCache[V]) Set(values []V) error

Set stores values in Redis and increments the version.

func (*RedisCache[V]) SetWithTTL

func (c *RedisCache[V]) SetWithTTL(values []V, ttl time.Duration) error

SetWithTTL stores values with a custom TTL.

func (*RedisCache[V]) TTL

func (c *RedisCache[V]) TTL() (time.Duration, error)

TTL returns the remaining TTL for the cache key.

type RedisConfig

type RedisConfig struct {
	// KeyPrefix is prepended to all Redis keys. Use a unique prefix per cache to avoid key collision.
	KeyPrefix string

	// VersionKeySuffix is appended to the key prefix for version tracking.
	// Default: ":version"
	VersionKeySuffix string

	// TTL is the default time-to-live for cached data. Must be positive; otherwise a default is used at Set time.
	// Default: 1 hour
	TTL time.Duration

	// OperationTimeout is the timeout for Redis operations.
	// Default: 5 seconds
	OperationTimeout time.Duration

	// MaxValueBytes limits the size of the value read from Redis in Get(). If <= 0, no limit is applied.
	// Default: 16MB. Prevents OOM from malicious or corrupted oversized values in Redis.
	MaxValueBytes int
}

RedisConfig holds configuration for Redis cache.

func DefaultRedisConfig

func DefaultRedisConfig() *RedisConfig

DefaultRedisConfig returns a default Redis configuration.

func (*RedisConfig) WithKeyPrefix

func (c *RedisConfig) WithKeyPrefix(prefix string) *RedisConfig

WithKeyPrefix sets the key prefix.

func (*RedisConfig) WithMaxValueBytes added in v1.1.0

func (c *RedisConfig) WithMaxValueBytes(n int) *RedisConfig

WithMaxValueBytes sets the maximum allowed size in bytes for a value read from Redis in Get(). Values larger than this are rejected to prevent OOM. Use 0 or negative to disable the limit.

func (*RedisConfig) WithOperationTimeout

func (c *RedisConfig) WithOperationTimeout(timeout time.Duration) *RedisConfig

WithOperationTimeout sets the operation timeout.

func (*RedisConfig) WithTTL

func (c *RedisConfig) WithTTL(ttl time.Duration) *RedisConfig

WithTTL sets the TTL.

func (*RedisConfig) WithVersionKeySuffix

func (c *RedisConfig) WithVersionKeySuffix(suffix string) *RedisConfig

WithVersionKeySuffix sets the version key suffix.

type ValidateFunc

type ValidateFunc[V any] func(value V) error

ValidateFunc defines a function that validates a value. Returns an error if the value is invalid.

Jump to

Keyboard shortcuts

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