main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
package main

import (
	"container/list"
	"fmt"
	"log"
	"sync"
	"time"
)

// CacheItem represents a single cache entry with expiration time
type CacheItem struct {
	key        string
	value      interface{}
	expiration time.Time
	listElement *list.Element // For LRU tracking
}

// Cache is a thread-safe in-memory cache with TTL and capacity limits
type Cache struct {
	mu           sync.RWMutex
	items        map[string]*CacheItem
	lruList      *list.List      // Tracks access order for LRU eviction
	maxCapacity  int             // Maximum number of items
	defaultTTL   time.Duration   // Default TTL for items
	cleanupTicker *time.Ticker   // Ticker for periodic expired item cleanup
}

// NewCache creates a new Cache with specified capacity and default TTL
func NewCache(maxCapacity int, defaultTTL time.Duration, cleanupInterval time.Duration) *Cache {
	c := &Cache{
		items:       make(map[string]*CacheItem),
		lruList:     list.New(),
		maxCapacity: maxCapacity,
		defaultTTL:  defaultTTL,
		cleanupTicker: time.NewTicker(cleanupInterval),
	}

	// Start background cleanup goroutine
	go c.startCleanupWorker()

	return c
}

// startCleanupWorker periodically removes expired items
func (c *Cache) startCleanupWorker() {
	for range c.cleanupTicker.C {
		c.cleanupExpired()
	}
}

// cleanupExpired removes all expired items from the cache
func (c *Cache) cleanupExpired() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	expiredCount := 0

	for key, item := range c.items {
		if item.expiration.Before(now) {
			c.lruList.Remove(item.listElement)
			delete(c.items, key)
			expiredCount++
		}
	}

	if expiredCount > 0 {
		log.Printf("Cleaned up %d expired cache items", expiredCount)
	}
}

// Set adds or updates an item in the cache with optional custom TTL
func (c *Cache) Set(key string, value interface{}, ttl ...time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Determine TTL (use custom if provided, else default)
	itemTTL := c.defaultTTL
	if len(ttl) > 0 {
		itemTTL = ttl[0]
	}
	expiration := time.Now().Add(itemTTL)

	// If key exists, update it and move to front of LRU list
	if existingItem, ok := c.items[key]; ok {
		existingItem.value = value
		existingItem.expiration = expiration
		c.lruList.MoveToFront(existingItem.listElement)
		return
	}

	// If at max capacity, evict least recently used item
	if c.lruList.Len() >= c.maxCapacity {
		evicted := c.lruList.Back()
		if evicted != nil {
			evictedItem := evicted.Value.(*CacheItem)
			delete(c.items, evictedItem.key)
			c.lruList.Remove(evicted)
			log.Printf("Evicted LRU item: %s", evictedItem.key)
		}
	}

	// Create new cache item
	item := &CacheItem{
		key:        key,
		value:      value,
		expiration: expiration,
	}
	// Add to LRU list and map
	item.listElement = c.lruList.PushFront(item)
	c.items[key] = item
}

// Get retrieves an item from the cache (returns nil if not found/expired)
func (c *Cache) Get(key string) interface{} {
	c.mu.RLock()
	item, ok := c.items[key]
	c.mu.RUnlock()

	// Check if item exists and is not expired
	if !ok || item.expiration.Before(time.Now()) {
		return nil
	}

	// Update LRU (move to front)
	c.mu.Lock()
	c.lruList.MoveToFront(item.listElement)
	c.mu.Unlock()

	return item.value
}

// Close stops the cleanup ticker and releases resources
func (c *Cache) Close() {
	c.cleanupTicker.Stop()
}

// Example usage
func main() {
	// Create cache: max 5 items, 10s default TTL, 5s cleanup interval
	cache := NewCache(5, 10*time.Second, 5*time.Second)
	defer cache.Close()

	// Add items to cache
	cache.Set("user:1", map[string]interface{}{"id": 1, "name": "Alice"})
	cache.Set("user:2", map[string]interface{}{"id": 2, "name": "Bob"})
	cache.Set("config:timeout", 30, 30*time.Second) // Custom TTL

	// Retrieve items
	if user1 := cache.Get("user:1"); user1 != nil {
		fmt.Printf("Found user1: %+v\n", user1)
	}

	if timeout := cache.Get("config:timeout"); timeout != nil {
		fmt.Printf("Found timeout config: %d\n", timeout.(int))
	}

	// Test expiration (short TTL item)
	cache.Set("temp:data", "short-lived", 1*time.Second)
	time.Sleep(2 * time.Second)
	if tempData := cache.Get("temp:data"); tempData == nil {
		fmt.Println("temp:data expired as expected")
	}

	// Test LRU eviction (add 6 items to exceed capacity of 5)
	for i := 3; i <= 7; i++ {
		cache.Set(fmt.Sprintf("user:%d", i), map[string]interface{}{"id": i, "name": fmt.Sprintf("User %d", i)})
	}

	// Check if least recently used (user:2) was evicted
	if user2 := cache.Get("user:2"); user2 == nil {
		fmt.Println("user:2 evicted (LRU) as expected")
	}
}

How It Works

Thread-safe in-memory cache with TTL expiration, cleanup ticker, and LRU-style eviction when capacity is reached.

Stores items in a map plus a linked list for LRU tracking; Set attaches expiration and moves keys to the front; Get checks expiry and updates recency; a background janitor evicts expired entries until Stop is called.

Key Concepts

  • 1Combination of map and list gives O(1) get and set with recency tracking.
  • 2TTL per entry and periodic cleanup prevent stale data buildup.
  • 3Capacity guard evicts the oldest items when the max size is exceeded.

When to Use This Pattern

  • Lightweight caching layer inside services without Redis.
  • Short-lived request caches for expensive computations.
  • Feature flag or configuration caching with expiration.

Best Practices

  • Stop the cleanup goroutine during shutdown to avoid leaks.
  • Tune TTL and capacity based on workload and memory budget.
  • Handle missing values gracefully to trigger fresh loads.
Go Version1.14
Difficultyadvanced
Production ReadyYes
Lines of Code175