kcache

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Dec 10, 2025 License: BSD-3-Clause Imports: 10 Imported by: 0

README

kcache

Go Reference Go Report Card License

A thread-safe in-memory key-value cache library for Go with expiration support and automatic cleanup.

Features

  • Thread-Safe: All operations are safe for concurrent use by multiple goroutines
  • Automatic Expiration: Set expiration times for cache items
  • Background Cleanup: Optional automatic cleanup of expired items via janitor goroutine
  • Type Flexible: Store values of any type
  • Numeric Operations: Increment and decrement numeric values atomically
  • Persistence: Serialize and deserialize cache data using Gob encoding
  • Eviction Callbacks: Set callbacks for when items are evicted from the cache
  • Batch Operations: Efficiently set, get, or delete multiple items in a single operation
  • Cache Statistics: Monitor cache performance with hit/miss rates and eviction counts
  • Key Pattern Operations: Find and delete keys by prefix for bulk management
  • TTL Management: Update expiration times and query remaining time-to-live
  • Atomic Operations: Thread-safe GetOrSet and CompareAndSwap operations

Installation

go get github.com/kydenul/kcache

Quick Start

package main

import (
    "fmt"
    "time"
    "github.com/kydenul/kcache"
)

func main() {
    // Create a cache with 5 minute default expiration and cleanup every 10 minutes
    c := kcache.New(5*time.Minute, 10*time.Minute)

    // Set a value with default expiration
    c.Set("key", "value", kcache.DefaultExpiration)

    // Get a value
    if val, found := c.Get("key"); found {
        fmt.Println(val)
    }

    // Set a value that never expires
    c.Set("permanent", "data", kcache.NoExpiration)

    // Set a value with custom expiration
    c.Set("custom", "expires in 1 minute", 1*time.Minute)
}

Core Features

Basic Operations
// Create a cache
c := kcache.New(5*time.Minute, 10*time.Minute)

// Set a value
c.Set("key", "value", kcache.DefaultExpiration)
c.SetDefault("key", "value") // Use default expiration

// Get a value
value, found := c.Get("key")

// Get a value with its expiration time
value, expiration, found := c.GetWithExpiration("key")

// Delete a value
c.Delete("key")

// Clear all cache items
c.Flush()

// Get the number of items in cache
count := c.ItemCount()
Conditional Operations
// Add only if the key doesn't exist
err := c.Add("key", "value", kcache.DefaultExpiration)
if errors.Is(err, kcache.ErrItemExists) {
    // Key already exists
}

// Replace only if the key exists
err := c.Replace("key", "new value", kcache.DefaultExpiration)
if errors.Is(err, kcache.ErrItemNotFound) {
    // Key doesn't exist
}
Numeric Operations
// Set a counter
c.Set("counter", 10, kcache.NoExpiration)

// Increment the value
err := c.Increment("counter", 5)  // counter is now 15

// Decrement the value
err := c.Decrement("counter", 3)  // counter is now 12

// Supported numeric types:
// int, int8, int16, int32, int64
// uint, uintptr, uint8, uint16, uint32, uint64
// float32, float64
Expiration Management
// Manually delete all expired items
c.DeleteExpired()

// Set an eviction callback
c.OnEvicted(func(key string, value any) {
    fmt.Printf("Item %s was evicted\n", key)
})
Persistence
// Save to file
err := c.WriteFile("/tmp/cache.dat")

// Load from file
err := c.LoadFile("/tmp/cache.dat")

// Use io.Writer/io.Reader
var buf bytes.Buffer
err := c.Write(&buf)
err := c.Load(&buf)
Iterating Over Cache
// Get all non-expired items
items := c.Items()
for key, item := range items {
    fmt.Printf("%s: %v (expires: %v)\n", key, item.Object, item.Expiration)
}

Advanced Usage

Creating Cache from Existing Data
items := map[string]kcache.Item{
    "key1": {
        Object:     "value1",
        Expiration: 0, // Never expires
    },
    "key2": {
        Object:     "value2",
        Expiration: time.Now().Add(5 * time.Minute).UnixNano(),
    },
}

c := kcache.NewFrom(5*time.Minute, 10*time.Minute, items)
Disabling Automatic Cleanup
// No background cleanup, must call DeleteExpired() manually
c := kcache.New(5*time.Minute, 0)

// Manual cleanup
c.DeleteExpired()
Cache Without Expiration
// Create a cache with no default expiration
c := kcache.New(kcache.NoExpiration, 0)

// All items won't expire unless explicitly set
c.Set("key", "value", kcache.DefaultExpiration) // Won't expire
c.Set("temp", "data", 1*time.Minute)            // Expires in 1 minute

Advanced Features

Batch Operations

Batch operations are more efficient than individual operations because they acquire the lock only once:

// Set multiple items at once
items := map[string]any{
    "user:1": "Alice",
    "user:2": "Bob",
    "user:3": "Charlie",
}
c.SetMultiple(items, 5*time.Minute)

// Get multiple items at once
keys := []string{"user:1", "user:2", "user:3"}
results := c.GetMultiple(keys)
for key, value := range results {
    fmt.Printf("%s: %v\n", key, value)
}

// Delete multiple items at once
c.DeleteMultiple(keys)
Cache Statistics

Monitor cache performance to optimize your application:

// Get current statistics
stats := c.Stats()
fmt.Printf("Hits: %d\n", stats.Hits)
fmt.Printf("Misses: %d\n", stats.Misses)
fmt.Printf("Evictions: %d\n", stats.Evictions)
fmt.Printf("Items: %d\n", stats.Items)
fmt.Printf("Hit Rate: %.2f%%\n", stats.HitRate*100)

// Reset statistics
c.ResetStats()
Key Pattern Operations

Manage related cache items using prefix-based operations:

// Get all keys in the cache
allKeys := c.Keys()
fmt.Printf("Total keys: %d\n", len(allKeys))

// Get all keys with a specific prefix
userKeys := c.KeysByPrefix("user:")
for _, key := range userKeys {
    fmt.Println(key)  // e.g., "user:1", "user:2", etc.
}

// Delete all keys with a specific prefix
deletedCount := c.DeleteByPrefix("session:")
fmt.Printf("Deleted %d session entries\n", deletedCount)
TTL Management

Update expiration times and query remaining time-to-live:

// Extend the expiration of an item (sliding expiration)
err := c.Touch("session:123", 30*time.Minute)
if errors.Is(err, kcache.ErrItemNotFound) {
    fmt.Println("Session not found or expired")
}

// Check how much time remains before expiration
ttl, err := c.GetTTL("session:123")
if err == nil {
    if ttl == -1 {
        fmt.Println("Session never expires")
    } else {
        fmt.Printf("Session expires in %v\n", ttl)
    }
}
Atomic Operations

Perform thread-safe conditional operations:

// Get existing value or set a new one atomically
value, wasSet := c.GetOrSet("counter", 0, kcache.NoExpiration)
if wasSet {
    fmt.Println("Counter initialized to 0")
} else {
    fmt.Printf("Counter already exists: %v\n", value)
}

// Compare-and-swap: update only if current value matches expected
success := c.CompareAndSwap("counter", 5, 6, kcache.NoExpiration)
if success {
    fmt.Println("Counter updated from 5 to 6")
} else {
    fmt.Println("Counter was not 5, update failed")
}

Performance Considerations

  • All operations use read-write locks for protection; read operations can execute concurrently
  • Background janitor runs in a separate goroutine and doesn't block main operations
  • Uses runtime.SetFinalizer to ensure the janitor stops when the cache is garbage collected
  • Adjust cleanup interval based on your use case to avoid overly frequent cleanup
  • Batch operations (SetMultiple, GetMultiple, DeleteMultiple) are significantly more efficient than individual operations when working with multiple items
  • Statistics tracking uses atomic operations for minimal performance overhead
  • Cache statistics are updated atomically without blocking cache operations

Best Practices

Use Batch Operations for Multiple Items
// ❌ Less efficient - multiple lock acquisitions
for _, key := range keys {
    c.Set(key, values[key], expiration)
}

// ✅ More efficient - single lock acquisition
c.SetMultiple(values, expiration)
Monitor Cache Performance
// Periodically check cache statistics to optimize expiration settings
ticker := time.NewTicker(1 * time.Minute)
go func() {
    for range ticker.C {
        stats := c.Stats()
        if stats.HitRate < 0.8 {
            log.Printf("Low hit rate: %.2f%% - consider adjusting TTL", stats.HitRate*100)
        }
    }
}()
Implement Sliding Expiration with Touch
// Extend expiration for actively used items
value, found := c.Get("session:123")
if found {
    // Reset the expiration timer
    c.Touch("session:123", 30*time.Minute)
}
Use Atomic Operations for Concurrent Updates
// Initialize a value only once across multiple goroutines
value, wasSet := c.GetOrSet("config", loadConfig(), kcache.NoExpiration)

// Safe concurrent counter updates
for {
    current, found := c.Get("counter")
    if !found {
        current = 0
    }
    if c.CompareAndSwap("counter", current, current.(int)+1, kcache.NoExpiration) {
        break // Successfully incremented
    }
    // Retry if another goroutine modified the value
}

Error Handling

The library defines the following error types:

kcache.ErrItemNotFound  // Item doesn't exist or has expired
kcache.ErrItemExists    // Item already exists (Add operation)
kcache.ErrNotNumeric    // Value is not a numeric type (Increment/Decrement operations)

Usage example:

err := c.Add("key", "value", kcache.DefaultExpiration)
if errors.Is(err, kcache.ErrItemExists) {
    // Handle key already exists
}

Thread Safety

All public methods are thread-safe and can be used safely from multiple goroutines:

c := kcache.New(5*time.Minute, 10*time.Minute)

// Concurrent access from multiple goroutines
for i := 0; i < 100; i++ {
    go func(n int) {
        c.Set(fmt.Sprintf("key%d", n), n, kcache.DefaultExpiration)
    }(i)
}

License

This project is licensed under the MIT License. See the LICENSE file for details.

Contributing

Issues and Pull Requests are welcome!

Acknowledgments

This project is inspired by go-cache.

Documentation

Overview

Package kcache provides a thread-safe in-memory key-value cache with expiration support.

The cache supports automatic expiration of items and optional background cleanup via a janitor goroutine. It is safe for concurrent use by multiple goroutines.

Example usage:

// Create a cache with 5 minute default expiration and cleanup every 10 minutes
c := kcache.New(5*time.Minute, 10*time.Minute)

// Set a value with default expiration
c.Set("key", "value", kcache.DefaultExpiration)

// Get a value
if val, found := c.Get("key"); found {
    fmt.Println(val)
}

// Set a value that never expires
c.Set("permanent", "data", kcache.NoExpiration)
Example (AtomicCounter)

Example_atomicCounter demonstrates implementing a thread-safe counter.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Initialize counter if it doesn't exist
	c.GetOrSet("page_views", 0, kcache.NoExpiration)

	// Atomic increment using compare-and-swap
	for {
		current, found := c.Get("page_views")
		if !found {
			current = 0
		}
		newValue := current.(int) + 1
		if c.CompareAndSwap("page_views", current, newValue, kcache.NoExpiration) {
			fmt.Printf("Page views: %d\n", newValue)
			break
		}
		// If CAS failed, another goroutine updated it, retry
	}
}
Output:
Page views: 1
Example (BatchOperations)

Example_batchOperations demonstrates efficient batch processing.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Efficiently load multiple items
	users := map[string]any{
		"user:1": map[string]string{"name": "Alice", "role": "admin"},
		"user:2": map[string]string{"name": "Bob", "role": "user"},
		"user:3": map[string]string{"name": "Charlie", "role": "user"},
	}
	c.SetMultiple(users, 10*time.Minute)

	// Efficiently retrieve multiple items
	keys := []string{"user:1", "user:2", "user:3"}
	results := c.GetMultiple(keys)

	fmt.Printf("Loaded %d users\n", len(results))
}
Output:
Loaded 3 users
Example (CacheMonitoring)

Example_cacheMonitoring demonstrates monitoring cache performance.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Simulate some cache operations
	c.Set("key1", "value1", kcache.DefaultExpiration)
	c.Get("key1") // Hit
	c.Get("key2") // Miss

	// Check performance
	stats := c.Stats()
	if stats.HitRate < 0.8 {
		log.Printf("Warning: Low cache hit rate: %.2f%%", stats.HitRate*100)
	}

	fmt.Printf("Cache efficiency: %.0f%%\n", stats.HitRate*100)
}
Output:
Cache efficiency: 50%
Example (PrefixOperations)

Example_prefixOperations demonstrates managing related cache entries.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Store session data
	c.Set("session:user1:token", "token1", kcache.DefaultExpiration)
	c.Set("session:user1:data", "data1", kcache.DefaultExpiration)
	c.Set("session:user2:token", "token2", kcache.DefaultExpiration)

	// Find all sessions for user1
	user1Sessions := c.KeysByPrefix("session:user1:")
	fmt.Printf("User1 sessions: %d\n", len(user1Sessions))

	// Logout user1 - delete all their sessions
	deleted := c.DeleteByPrefix("session:user1:")
	fmt.Printf("Deleted %d session entries\n", deleted)
}
Output:
User1 sessions: 2
Deleted 2 session entries
Example (SlidingExpiration)

Example_slidingExpiration demonstrates implementing sliding expiration pattern.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Set initial session
	c.Set("session:123", "user_data", 30*time.Minute)

	// On each access, extend the expiration
	if value, found := c.Get("session:123"); found {
		// User is active, extend session
		c.Touch("session:123", 30*time.Minute)
		fmt.Printf("Session active: %v\n", value)
	}
}
Output:
Session active: user_data

Index

Examples

Constants

View Source
const (
	// NoExpiration indicates that a cache item should never expire.
	// Use this value when calling Set, Add, or Replace to create items
	// that persist until explicitly deleted.
	NoExpiration time.Duration = -1

	// DefaultExpiration indicates that the cache's default expiration time
	// should be used. The default expiration is set when creating a new cache
	// with New or NewFrom.
	DefaultExpiration time.Duration = 0
)

Variables

View Source
var (
	// ErrItemNotFound is returned when attempting to access a cache item that doesn't exist or has expired.
	ErrItemNotFound = errors.New("item not found")

	// ErrItemExists is returned by Add when attempting to add an item with a key that already exists.
	ErrItemExists = errors.New("item already exists")

	// ErrNotNumeric is returned by Increment or Decrement when the cached value is not a numeric type.
	ErrNotNumeric = errors.New("item value is not a numeric type")
)

Functions

This section is empty.

Types

type Cache

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

Cache is a thread-safe in-memory key-value store with expiration support. It wraps the internal cache implementation and provides a public API for cache operations. All methods are safe for concurrent use.

func New

func New(defaultExpiration, cleanupInterval time.Duration) *Cache

New creates and returns a new cache with the specified default expiration duration and cleanup interval.

Parameters:

  • defaultExpiration: The default expiration duration for cache items. If set to 0 or negative, items will not expire by default unless explicitly set with an expiration.
  • cleanupInterval: How often the janitor goroutine runs to remove expired items. If less than or equal to 0, no automatic cleanup occurs and expired items must be removed manually by calling DeleteExpired().

Returns a new Cache instance ready for use.

Example:

// Cache with 5 minute default expiration, cleanup every 10 minutes
c := kcache.New(5*time.Minute, 10*time.Minute)

// Cache with no expiration and no automatic cleanup
c := kcache.New(kcache.NoExpiration, 0)

func NewFrom

func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache

NewFrom creates and returns a new cache initialized with the provided items map.

This function is useful for:

  • Restoring a cache from a previously serialized state (e.g., loaded via Load or LoadFile)
  • Pre-allocating a cache with a known minimum size for better performance
  • Starting with a pre-populated set of cache items

Parameters:

  • defaultExpiration: The default expiration duration for cache items. If set to 0 or negative, items will not expire by default unless explicitly set with an expiration.
  • cleanupInterval: How often the janitor goroutine runs to remove expired items. If less than or equal to 0, no automatic cleanup occurs.
  • items: A map of cache items to initialize the cache with. The map will be used directly as the underlying storage (not copied).

Returns a new Cache instance initialized with the provided items.

Example:

// Create cache from deserialized items
items := make(map[string]kcache.Item)
// ... load items from file ...
c := kcache.NewFrom(5*time.Minute, 10*time.Minute, items)

func (Cache) Add

func (c Cache) Add(key string, value any, expiration time.Duration) error

Add adds a new item to the cache only if the key doesn't already exist.

If the key already exists (even if expired), this method returns an error and does not update the value.

Parameters:

  • key: The cache key
  • value: The value to cache (can be any type)
  • expiration: The expiration duration (same semantics as Set)

Returns ErrItemExists if the key already exists, nil otherwise.

This method is safe for concurrent use.

Example:

err := c.Add("user:123", userData, 5*time.Minute)
if errors.Is(err, kcache.ErrItemExists) {
    // Key already exists
}

func (Cache) CompareAndSwap

func (c Cache) CompareAndSwap(key string, oldValue, newValue any, expiration time.Duration) bool

CompareAndSwap atomically compares the current value with an expected value and swaps it with a new value if they match.

This method implements a compare-and-swap (CAS) operation under a write lock, ensuring thread safety in concurrent environments. The comparison uses deep equality checking via reflect.DeepEqual.

Parameters:

  • key: The cache key
  • oldValue: The expected current value
  • newValue: The new value to set if the comparison succeeds
  • expiration: The expiration duration for the new value. Special values:
  • DefaultExpiration (0): Use the cache's default expiration time
  • NoExpiration (-1): Item never expires
  • Any positive duration: Item expires after that duration from now

Returns:

  • true if the comparison succeeded and the value was swapped
  • false if the key doesn't exist, has expired, or the current value doesn't match oldValue

This method is safe for concurrent use.

Example:

// Atomically update a counter only if it has the expected value
success := c.CompareAndSwap("counter", 5, 6, kcache.NoExpiration)
if success {
    fmt.Println("Counter updated from 5 to 6")
} else {
    fmt.Println("Counter value was not 5, update failed")
}
Example

ExampleCache_CompareAndSwap demonstrates atomic compare-and-swap operation.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("counter", 5, kcache.NoExpiration)

	// Try to update from 5 to 6 - should succeed
	success := c.CompareAndSwap("counter", 5, 6, kcache.NoExpiration)
	fmt.Printf("Update 5->6: %v\n", success)

	// Try to update from 5 to 7 - should fail (current value is 6)
	success = c.CompareAndSwap("counter", 5, 7, kcache.NoExpiration)
	fmt.Printf("Update 5->7: %v\n", success)

	// Verify final value
	if val, found := c.Get("counter"); found {
		fmt.Printf("Final value: %v\n", val)
	}
}
Output:
Update 5->6: true
Update 5->7: false
Final value: 6

func (Cache) Decrement

func (c Cache) Decrement(key string, n int64) error

Decrement subtracts a delta from a numeric value in the cache.

The cached value must be one of the supported numeric types: int, int8, int16, int32, int64, uint, uintptr, uint8, uint16, uint32, uint64, float32, float64

Parameters:

  • key: The cache key
  • n: The delta to subtract (can be negative to add)

Returns:

  • ErrItemNotFound if the key doesn't exist or has expired
  • ErrNotNumeric if the cached value is not a supported numeric type

Note: This operation can overflow/underflow for integer types.

This method is safe for concurrent use.

Example:

c.Set("counter", 10, kcache.NoExpiration)
err := c.Decrement("counter", 3)  // counter is now 7
if errors.Is(err, kcache.ErrNotNumeric) {
    // Value is not numeric
}

func (Cache) Delete

func (c Cache) Delete(key string)

Delete removes an item from the cache.

If the key doesn't exist, this method does nothing (no error is returned). If an OnEvicted callback is set, it will be called with the deleted key and value.

Parameters:

  • key: The cache key to delete

This method is safe for concurrent use.

func (Cache) DeleteByPrefix

func (c Cache) DeleteByPrefix(prefix string) int

DeleteByPrefix removes all items from the cache whose keys start with the given prefix.

This method is useful for bulk deletion of related cache items. If an OnEvicted callback is set, it will be called for each deleted item after the lock is released.

Parameters:

  • prefix: The prefix string to match against cache keys

Returns the number of items deleted.

This method is safe for concurrent use.

Example:

// Delete all user-related cache entries
count := c.DeleteByPrefix("user:")
fmt.Printf("Deleted %d user entries\n", count)
Example

ExampleCache_DeleteByPrefix demonstrates bulk deletion by prefix.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("temp:1", "data1", kcache.DefaultExpiration)
	c.Set("temp:2", "data2", kcache.DefaultExpiration)
	c.Set("perm:1", "data3", kcache.DefaultExpiration)

	// Delete all temporary items
	deleted := c.DeleteByPrefix("temp:")
	fmt.Printf("Deleted %d items\n", deleted)
	fmt.Printf("Remaining items: %d\n", c.ItemCount())
}
Output:
Deleted 2 items
Remaining items: 1

func (Cache) DeleteExpired

func (c Cache) DeleteExpired()

DeleteExpired removes all expired items from the cache.

This method scans all items in the cache and removes those that have expired. If an OnEvicted callback is set, it will be called for each expired item.

This method is automatically called by the janitor goroutine if automatic cleanup is enabled. It can also be called manually for on-demand cleanup.

This method is safe for concurrent use.

func (Cache) DeleteMultiple

func (c Cache) DeleteMultiple(keys []string)

DeleteMultiple removes multiple items from the cache in a single operation.

This method is more efficient than calling Delete multiple times because it acquires the write lock only once for all items.

If an OnEvicted callback is set, it will be called for each deleted item after the lock is released.

Parameters:

  • keys: A slice of keys to delete from the cache

This method is safe for concurrent use.

Example:

keys := []string{"user:1", "user:2", "user:3"}
c.DeleteMultiple(keys)
Example

ExampleCache_DeleteMultiple demonstrates batch deletion of multiple cache items.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Set up some test data
	c.Set("temp:1", "data1", kcache.DefaultExpiration)
	c.Set("temp:2", "data2", kcache.DefaultExpiration)
	c.Set("temp:3", "data3", kcache.DefaultExpiration)

	fmt.Printf("Items before: %d\n", c.ItemCount())

	// Delete multiple items at once
	keys := []string{"temp:1", "temp:2", "temp:3"}
	c.DeleteMultiple(keys)

	fmt.Printf("Items after: %d\n", c.ItemCount())
}
Output:
Items before: 3
Items after: 0

func (Cache) Flush

func (c Cache) Flush()

Flush removes all items from the cache.

This operation clears the entire cache. Unlike Delete() and DeleteExpired(), the OnEvicted callback is NOT called for items removed by Flush.

This method is safe for concurrent use.

func (Cache) Get

func (c Cache) Get(key string) (any, bool)

Get retrieves an item from the cache by key.

Returns the cached value and true if the key exists and has not expired. Returns nil and false if the key doesn't exist or has expired.

Expired items are not automatically removed by this method; they are simply treated as non-existent. Use DeleteExpired() or enable automatic cleanup to remove expired items from memory.

This method is safe for concurrent use and updates cache statistics.

func (Cache) GetMultiple

func (c Cache) GetMultiple(keys []string) map[string]any

GetMultiple retrieves multiple items from the cache in a single operation.

This method is more efficient than calling Get multiple times because it acquires the read lock only once for all items.

Parameters:

  • keys: A slice of keys to retrieve from the cache

Returns a map containing only the keys that exist and have not expired. Keys that don't exist or have expired are not included in the result.

This method is safe for concurrent use and updates cache statistics.

Example:

keys := []string{"user:1", "user:2", "user:3"}
results := c.GetMultiple(keys)
for key, value := range results {
    fmt.Printf("%s: %v\n", key, value)
}
Example

ExampleCache_GetMultiple demonstrates batch retrieval of multiple cache items.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Set up some test data
	c.Set("user:1", "Alice", kcache.DefaultExpiration)
	c.Set("user:2", "Bob", kcache.DefaultExpiration)
	c.Set("user:3", "Charlie", kcache.DefaultExpiration)

	// Get multiple items at once
	keys := []string{"user:1", "user:2", "user:3", "user:4"}
	results := c.GetMultiple(keys)

	// user:4 doesn't exist, so it won't be in results
	fmt.Printf("Found %d items\n", len(results))
	fmt.Println(results["user:1"])
}
Output:
Found 3 items
Alice

func (Cache) GetOrSet

func (c Cache) GetOrSet(key string, value any, expiration time.Duration) (any, bool)

GetOrSet atomically gets an existing value or sets a new value if the key doesn't exist.

This method performs a get-or-set operation in a single atomic operation under a write lock, ensuring thread safety in concurrent environments.

Parameters:

  • key: The cache key
  • value: The value to set if the key doesn't exist
  • expiration: The expiration duration for the new value. Special values:
  • DefaultExpiration (0): Use the cache's default expiration time
  • NoExpiration (-1): Item never expires
  • Any positive duration: Item expires after that duration from now

Returns:

  • The actual value in the cache (either existing or newly set)
  • true if a new value was set, false if an existing value was returned

This method is safe for concurrent use and updates cache statistics.

Example:

// Initialize a counter if it doesn't exist
value, wasSet := c.GetOrSet("counter", 0, kcache.NoExpiration)
if wasSet {
    fmt.Println("Counter initialized to 0")
} else {
    fmt.Printf("Counter already exists with value: %v\n", value)
}
Example

ExampleCache_GetOrSet demonstrates atomic get-or-set operation.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// First call sets the value
	value, wasSet := c.GetOrSet("counter", 0, kcache.NoExpiration)
	fmt.Printf("Value: %v, Was set: %v\n", value, wasSet)

	// Second call returns existing value
	value, wasSet = c.GetOrSet("counter", 100, kcache.NoExpiration)
	fmt.Printf("Value: %v, Was set: %v\n", value, wasSet)
}
Output:
Value: 0, Was set: true
Value: 0, Was set: false

func (Cache) GetTTL

func (c Cache) GetTTL(key string) (time.Duration, error)

GetTTL returns the remaining time-to-live for a cache item.

This method calculates how much time remains before the item expires.

Parameters:

  • key: The cache key to check

Returns:

  • The remaining duration until expiration
  • nil error if the key exists and hasn't expired
  • ErrItemNotFound if the key doesn't exist or has already expired

Special cases:

  • If the item has NoExpiration, returns -1 (indicating it never expires)
  • If the item has already expired, returns ErrItemNotFound

This method is safe for concurrent use.

Example:

ttl, err := c.GetTTL("session:123")
if err == nil {
    if ttl == -1 {
        fmt.Println("Session never expires")
    } else {
        fmt.Printf("Session expires in %v\n", ttl)
    }
}
Example

ExampleCache_GetTTL demonstrates checking remaining time-to-live.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("key", "value", 10*time.Second)

	ttl, err := c.GetTTL("key")
	if err == nil {
		fmt.Printf("Expires in approximately %v\n", ttl.Round(time.Second))
	}

	// Check item with no expiration
	c.Set("permanent", "data", kcache.NoExpiration)
	ttl, _ = c.GetTTL("permanent")
	if ttl == -1 {
		fmt.Println("Item never expires")
	}
}
Output:
Expires in approximately 10s
Item never expires

func (Cache) GetWithExpiration

func (c Cache) GetWithExpiration(key string) (any, time.Time, bool)

GetWithExpiration retrieves an item from the cache along with its expiration time.

Returns:

  • The cached value
  • The expiration time as a time.Time (zero value if item never expires)
  • true if the key exists and has not expired, false otherwise

If the item has no expiration (NoExpiration), the returned time.Time will be the zero value (time.Time{}).

This method is safe for concurrent use.

func (Cache) Increment

func (c Cache) Increment(key string, n int64) error

Increment adds a delta to a numeric value in the cache.

The cached value must be one of the supported numeric types: int, int8, int16, int32, int64, uint, uintptr, uint8, uint16, uint32, uint64, float32, float64

Parameters:

  • key: The cache key
  • n: The delta to add (can be negative to subtract)

Returns:

  • ErrItemNotFound if the key doesn't exist or has expired
  • ErrNotNumeric if the cached value is not a supported numeric type

Note: This operation can overflow/underflow for integer types.

This method is safe for concurrent use.

Example:

c.Set("counter", 10, kcache.NoExpiration)
err := c.Increment("counter", 5)  // counter is now 15
if errors.Is(err, kcache.ErrNotNumeric) {
    // Value is not numeric
}

func (Cache) ItemCount

func (c Cache) ItemCount() int

ItemCount returns the total number of items in the cache.

Note: This count may include expired items that haven't been cleaned up yet. To get an accurate count of only valid items, call DeleteExpired() first or use len(c.Items()).

This method is safe for concurrent use.

func (Cache) Items

func (c Cache) Items() map[string]Item

Items returns a copy of all non-expired items in the cache.

The returned map is a snapshot of the cache at the time of the call. Expired items are automatically filtered out and not included in the result. Modifying the returned map does not affect the cache.

Returns a new map containing copies of all non-expired cache items.

This method is safe for concurrent use.

Example:

items := c.Items()
for key, item := range items {
    fmt.Printf("%s: %v\n", key, item.Object)
}

func (Cache) Keys

func (c Cache) Keys() []string

Keys returns a slice of all non-expired keys in the cache.

Expired items are automatically filtered out and not included in the result. The order of keys in the returned slice is not guaranteed.

Returns a slice containing all non-expired cache keys.

This method is safe for concurrent use.

Example:

keys := c.Keys()
fmt.Printf("Cache contains %d keys\n", len(keys))
for _, key := range keys {
    fmt.Println(key)
}
Example

ExampleCache_Keys demonstrates retrieving all cache keys.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("user:1", "Alice", kcache.DefaultExpiration)
	c.Set("user:2", "Bob", kcache.DefaultExpiration)
	c.Set("session:1", "data", kcache.DefaultExpiration)

	keys := c.Keys()
	fmt.Printf("Total keys: %d\n", len(keys))
}
Output:
Total keys: 3

func (Cache) KeysByPrefix

func (c Cache) KeysByPrefix(prefix string) []string

KeysByPrefix returns a slice of all non-expired keys that start with the given prefix.

Expired items are automatically filtered out and not included in the result. The order of keys in the returned slice is not guaranteed.

Parameters:

  • prefix: The prefix string to match against cache keys

Returns a slice containing all non-expired cache keys that start with the prefix. If no keys match the prefix, an empty slice is returned.

This method is safe for concurrent use.

Example:

// Get all user-related keys
userKeys := c.KeysByPrefix("user:")
for _, key := range userKeys {
    fmt.Println(key)  // e.g., "user:1", "user:2", etc.
}
Example

ExampleCache_KeysByPrefix demonstrates finding keys by prefix.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("user:1", "Alice", kcache.DefaultExpiration)
	c.Set("user:2", "Bob", kcache.DefaultExpiration)
	c.Set("session:1", "data", kcache.DefaultExpiration)

	// Get only user keys
	userKeys := c.KeysByPrefix("user:")
	fmt.Printf("User keys: %d\n", len(userKeys))
}
Output:
User keys: 2

func (Cache) Load

func (c Cache) Load(r io.Reader) error

Load deserializes cache items from an io.Reader and adds them to the cache.

Items are loaded using Gob decoding. If a key already exists in the cache and hasn't expired, it will NOT be overwritten by the loaded value. Only new keys or keys with expired values will be updated.

Parameters:

  • r: The reader to deserialize from

Returns an error if deserialization fails.

This method is safe for concurrent use (the cache is locked during loading).

Example:

file, err := os.Open("/tmp/cache.dat")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
err = c.Load(file)

func (Cache) LoadFile

func (c Cache) LoadFile(filename string) error

LoadFile deserializes cache items from a file and adds them to the cache.

Items are loaded using Gob decoding. If a key already exists in the cache and hasn't expired, it will NOT be overwritten by the loaded value. Only new keys or keys with expired values will be updated.

Parameters:

  • filename: The path to the file to read from

Returns an error if the file cannot be opened or if deserialization fails.

This method is safe for concurrent use (the cache is locked during loading).

Example:

err := c.LoadFile("/tmp/cache.dat")
if err != nil {
    log.Fatal(err)
}

func (Cache) OnEvicted

func (c Cache) OnEvicted(fn func(key string, value any))

OnEvicted sets a callback function that is called when an item is evicted from the cache.

The callback is invoked when items are removed via:

  • Delete() - explicit deletion
  • DeleteExpired() - automatic or manual cleanup of expired items

The callback is NOT invoked when items are removed via:

  • Flush() - bulk deletion of all items
  • Replace() - updating an existing item

Parameters:

  • fn: A function that receives the key and value of the evicted item. The function is called outside of the cache lock, so it's safe to perform cache operations within the callback.

This method is safe for concurrent use.

Example:

c.OnEvicted(func(key string, value any) {
    log.Printf("Item %s was evicted", key)
})

func (Cache) Replace

func (c Cache) Replace(key string, val any, expiration time.Duration) error

Replace updates an existing item in the cache.

If the key doesn't exist or has expired, this method returns an error and does not create a new item.

Parameters:

  • key: The cache key
  • val: The new value to cache (can be any type)
  • expiration: The new expiration duration (same semantics as Set)

Returns ErrItemNotFound if the key doesn't exist or has expired, nil otherwise.

This method is safe for concurrent use.

Example:

err := c.Replace("user:123", newUserData, 5*time.Minute)
if errors.Is(err, kcache.ErrItemNotFound) {
    // Key doesn't exist
}

func (Cache) ResetStats

func (c Cache) ResetStats()

ResetStats resets all cache statistics counters to zero.

This method resets the hits, misses, and evictions counters but does not affect the actual cache contents or the item count.

This method is safe for concurrent use.

Example:

c.ResetStats()
// All statistics counters are now zero
Example

ExampleCache_ResetStats demonstrates resetting cache statistics.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("key", "value", kcache.DefaultExpiration)
	c.Get("key") // Hit

	stats := c.Stats()
	fmt.Printf("Hits before reset: %d\n", stats.Hits)

	c.ResetStats()

	stats = c.Stats()
	fmt.Printf("Hits after reset: %d\n", stats.Hits)
}
Output:
Hits before reset: 1
Hits after reset: 0

func (Cache) Set

func (c Cache) Set(key string, value any, expiration time.Duration)

Set adds or updates an item in the cache.

If the key already exists, its value and expiration are updated. If the key doesn't exist, a new item is created.

Parameters:

  • key: The cache key
  • value: The value to cache (can be any type)
  • expiration: The expiration duration. Special values:
  • DefaultExpiration (0): Use the cache's default expiration time
  • NoExpiration (-1): Item never expires
  • Any positive duration: Item expires after that duration from now

This method is safe for concurrent use.

Example:

c.Set("user:123", userData, 5*time.Minute)           // Expires in 5 minutes
c.Set("config", configData, kcache.NoExpiration)     // Never expires
c.Set("temp", tempData, kcache.DefaultExpiration)    // Uses cache default

func (Cache) SetDefault

func (c Cache) SetDefault(k string, value any)

SetDefault adds or updates an item in the cache using the default expiration time.

This is a convenience method equivalent to calling Set with DefaultExpiration.

Parameters:

  • k: The cache key
  • value: The value to cache (can be any type)

This method is safe for concurrent use.

func (Cache) SetMultiple

func (c Cache) SetMultiple(items map[string]any, expiration time.Duration)

SetMultiple adds or updates multiple items in the cache in a single operation.

This method is more efficient than calling Set multiple times because it acquires the write lock only once for all items.

Parameters:

  • items: A map of key-value pairs to set in the cache
  • expiration: The expiration duration for all items. Special values:
  • DefaultExpiration (0): Use the cache's default expiration time
  • NoExpiration (-1): Items never expire
  • Any positive duration: Items expire after that duration from now

This method is safe for concurrent use.

Example:

items := map[string]any{
    "user:1": userData1,
    "user:2": userData2,
    "user:3": userData3,
}
c.SetMultiple(items, 5*time.Minute)
Example

ExampleCache_SetMultiple demonstrates batch setting of multiple cache items.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Set multiple items at once - more efficient than individual Set calls
	items := map[string]any{
		"user:1": "Alice",
		"user:2": "Bob",
		"user:3": "Charlie",
	}
	c.SetMultiple(items, 5*time.Minute)

	// Verify items were set
	if val, found := c.Get("user:1"); found {
		fmt.Println(val)
	}
}
Output:
Alice

func (Cache) Stats

func (c Cache) Stats() Statistics

Stats returns a snapshot of the current cache statistics.

The returned Statistics struct includes:

  • Hits: Number of successful cache retrievals
  • Misses: Number of failed cache retrievals (key not found or expired)
  • Evictions: Number of items evicted from the cache
  • Items: Current number of items in the cache
  • HitRate: Cache hit rate calculated as hits / (hits + misses), or 0 if no operations

This method is safe for concurrent use.

Example:

stats := c.Stats()
fmt.Printf("Hit rate: %.2f%%\n", stats.HitRate*100)
Example

ExampleCache_Stats demonstrates monitoring cache performance.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	// Perform some operations
	c.Set("key1", "value1", kcache.DefaultExpiration)
	c.Set("key2", "value2", kcache.DefaultExpiration)

	c.Get("key1") // Hit
	c.Get("key2") // Hit
	c.Get("key3") // Miss

	// Get statistics
	stats := c.Stats()
	fmt.Printf("Hits: %d\n", stats.Hits)
	fmt.Printf("Misses: %d\n", stats.Misses)
	fmt.Printf("Hit Rate: %.0f%%\n", stats.HitRate*100)
}
Output:
Hits: 2
Misses: 1
Hit Rate: 67%

func (Cache) Touch

func (c Cache) Touch(key string, expiration time.Duration) error

Touch updates the expiration time of an existing cache item without changing its value.

This method is useful for extending the lifetime of cache items that are still being actively used, implementing a "sliding expiration" pattern.

Parameters:

  • key: The cache key to update
  • expiration: The new expiration duration. Special values:
  • DefaultExpiration (0): Use the cache's default expiration time
  • NoExpiration (-1): Item never expires
  • Any positive duration: Item expires after that duration from now

Returns ErrItemNotFound if the key doesn't exist or has already expired.

This method is safe for concurrent use.

Example:

// Extend the expiration of a session by 30 minutes
err := c.Touch("session:123", 30*time.Minute)
if errors.Is(err, kcache.ErrItemNotFound) {
    // Session doesn't exist or has expired
}
Example

ExampleCache_Touch demonstrates updating item expiration time.

package main

import (
	"fmt"
	"time"

	"github.com/kydenul/kcache"
)

func main() {
	c := kcache.New(5*time.Minute, 10*time.Minute)

	c.Set("session:123", "user_data", 1*time.Second)

	// Extend the expiration before it expires
	time.Sleep(500 * time.Millisecond)
	err := c.Touch("session:123", 5*time.Minute)
	if err == nil {
		fmt.Println("Session expiration extended")
	}
}
Output:
Session expiration extended

func (Cache) Write

func (c Cache) Write(w io.Writer) (err error)

Write serializes the cache's items to an io.Writer using Gob encoding.

All items in the cache (including expired ones) are serialized. The cache automatically registers all item types with the Gob encoder.

Parameters:

  • w: The writer to serialize to

Returns an error if serialization fails (e.g., if an item contains an unserializable type like a channel).

This method is safe for concurrent use (the cache is locked during serialization).

Example:

var buf bytes.Buffer
err := c.Write(&buf)
if err != nil {
    log.Fatal(err)
}

func (Cache) WriteFile

func (c Cache) WriteFile(filename string) error

WriteFile serializes the cache's items to a file using Gob encoding.

If the file doesn't exist, it will be created. If it exists, it will be overwritten.

Parameters:

  • filename: The path to the file to write to

Returns an error if the file cannot be created or if serialization fails.

This method is safe for concurrent use (the cache is locked during serialization).

Example:

err := c.WriteFile("/tmp/cache.dat")
if err != nil {
    log.Fatal(err)
}

type CacheStats

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

CacheStats holds statistics about cache operations. All fields use atomic operations for thread-safe updates.

type Item

type Item struct {
	// Object is the cached value. It can be of any type.
	Object any

	// Expiration is the Unix timestamp (in nanoseconds) when this item expires.
	// A value of 0 means the item never expires.
	Expiration int64
}

Item represents a single cache entry containing a value and its expiration time. Items are stored in the cache and can be retrieved, updated, or deleted.

func (Item) Expired

func (item Item) Expired() bool

Expired returns true if the item has expired, false otherwise. An item with Expiration set to 0 never expires and always returns false.

This method checks expiration against the current time.

type Statistics

type Statistics struct {
	Hits      uint64  // Number of cache hits
	Misses    uint64  // Number of cache misses
	Evictions uint64  // Number of evicted items
	Items     int     // Current number of items in cache
	HitRate   float64 // Cache hit rate (hits / (hits + misses))
}

Statistics represents a snapshot of cache statistics.

Jump to

Keyboard shortcuts

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