A thread-safe in-memory key-value cache library for Go with expiration support and automatic cleanup.
- 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
go get github.com/kydenul/kcachepackage 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)
}// 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()// 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
}// 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// 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)
})// 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)// 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)
}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)// No background cleanup, must call DeleteExpired() manually
c := kcache.New(5*time.Minute, 0)
// Manual cleanup
c.DeleteExpired()// 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 minuteBatch 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)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()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)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)
}
}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")
}- 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.SetFinalizerto 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
// ❌ Less efficient - multiple lock acquisitions
for _, key := range keys {
c.Set(key, values[key], expiration)
}
// ✅ More efficient - single lock acquisition
c.SetMultiple(values, expiration)// 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)
}
}
}()// 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)
}// 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
}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
}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)
}This project is licensed under the MIT License. See the LICENSE file for details.
Issues and Pull Requests are welcome!
This project is inspired by go-cache.