Implementing Cache Strategies in Go
Caching is a fundamental technique for improving the performance of applications by storing frequently accessed data in a faster, more accessible location. This challenge will test your ability to implement and manage different caching strategies in Go, focusing on common patterns like Least Recently Used (LRU) and First-In, First-Out (FIFO). A well-implemented cache can significantly reduce latency and database load.
Problem Description
You are tasked with building a generic cache system in Go that supports different eviction policies. The cache should be able to store key-value pairs, where keys and values can be of any type. You need to implement at least two common cache eviction strategies:
- Least Recently Used (LRU): When the cache reaches its capacity, the item that has not been accessed for the longest time is evicted.
- First-In, First-Out (FIFO): When the cache reaches its capacity, the item that was added to the cache first is evicted, regardless of its usage.
Your implementation should provide the following operations:
NewCache(capacity int, strategy string) (*Cache, error): Constructor to create a new cache with a specified capacity and eviction strategy. It should return an error if the strategy is invalid or capacity is non-positive.Set(key, value interface{}): Adds or updates a key-value pair in the cache. If the cache is full, an item will be evicted according to the chosen strategy before adding the new item.Get(key interface{}) (interface{}, bool): Retrieves a value associated with a key. Returns the value andtrueif the key exists, otherwise returnsnilandfalse. For LRU, accessing a key should mark it as recently used.Delete(key interface{}): Removes a key-value pair from the cache.Len() int: Returns the current number of items in the cache.
Key Requirements:
- The cache must be thread-safe. Multiple goroutines should be able to access and modify the cache concurrently without data corruption.
- The
strategyparameter inNewCacheshould accept"lru"and"fifo"(case-insensitive). - The
Getoperation for LRU must update the "recency" of the accessed item.
Edge Cases:
- Setting a key that already exists should update its value and, for LRU, reset its recency.
- Getting a non-existent key should return
nil, false. - Deleting a non-existent key should be a no-op.
- Cache capacity of 0 or negative should result in an error.
- An unknown eviction strategy should result in an error.
Examples
Example 1: LRU Cache
Input:
capacity = 2
strategy = "lru"
cache := NewCache(capacity, strategy)
cache.Set("a", 1) // Cache: {"a": 1}
cache.Set("b", 2) // Cache: {"a": 1, "b": 2}
val, ok := cache.Get("a") // val: 1, ok: true. Accessing "a" makes it most recently used.
// Cache state after Get("a"): {"b": 2, "a": 1} (logical order of recency: a, b)
cache.Set("c", 3) // Cache is full. "b" is least recently used, so it's evicted.
// Cache: {"a": 1, "c": 3}
val, ok = cache.Get("b") // val: nil, ok: false. "b" was evicted.
Example 2: FIFO Cache
Input:
capacity = 2
strategy = "fifo"
cache := NewCache(capacity, strategy)
cache.Set("a", 1) // Cache: {"a": 1}
cache.Set("b", 2) // Cache: {"a": 1, "b": 2}
cache.Set("c", 3) // Cache is full. "a" was added first, so it's evicted.
// Cache: {"b": 2, "c": 3}
val, ok := cache.Get("a") // val: nil, ok: false. "a" was evicted.
val, ok = cache.Get("b") // val: 2, ok: true.
Example 3: Edge Case - Updating and Deleting
Input:
capacity = 3
strategy = "lru"
cache := NewCache(capacity, strategy)
cache.Set("x", 10)
cache.Set("y", 20)
cache.Set("z", 30) // Cache: {"x": 10, "y": 20, "z": 30}
cache.Set("x", 100) // Update value of "x". For LRU, "x" becomes most recently used.
// Cache: {"y": 20, "z": 30, "x": 100} (logical order of recency: x, z, y)
cache.Delete("y") // Delete "y".
// Cache: {"z": 30, "x": 100}
val, ok := cache.Get("y") // val: nil, ok: false.
Constraints
- Cache capacity will be an integer between 1 and 100,000.
- Keys and values can be any Go
interface{}type. - The number of
SetandGetoperations per test case will not exceed 1,000,000. - The cache operations (
Set,Get,Delete) should aim for an average time complexity of O(1).
Notes
- For the LRU strategy, you'll likely need a data structure that allows efficient tracking of access order. A combination of a map for O(1) lookups and a doubly linked list for O(1) insertions/deletions/reordering might be suitable.
- For the FIFO strategy, a map for lookups and a queue (e.g., using a slice or
container/list) for tracking insertion order would be effective. - Remember to use mutexes (e.g.,
sync.RWMutex) to ensure thread safety for all cache operations. - Consider how to handle the case where a
nilkey or value is passed toSetorGet. The problem statement impliesinterface{}, sonilis a valid value. - Case-insensitivity for the
strategystring is important.