Implementing Cache Strategies in Go
Caching is a crucial technique for optimizing performance in many applications by storing frequently accessed data in a faster-access location. This challenge asks you to implement several common caching strategies in Go, demonstrating your understanding of cache eviction policies and their impact on performance. Successfully completing this challenge will provide a solid foundation for building efficient and scalable Go applications.
Problem Description
You are tasked with implementing a generic cache in Go that supports different eviction strategies. The cache should store key-value pairs, allowing retrieval, insertion, and deletion of data. The core requirement is to implement three eviction strategies: Least Recently Used (LRU), First-In, First-Out (FIFO), and Least Frequently Used (LFU).
What needs to be achieved:
- Create a generic
Cacheinterface that defines the basic cache operations:Get(key string) (interface{}, bool),Put(key string, value interface{}), andDelete(key string). - Implement three concrete cache structures:
LRUCache,FIFOCache, andLFUCache, each conforming to theCacheinterface. - Each cache implementation should utilize its respective eviction strategy when the cache reaches its maximum capacity.
Key Requirements:
- Genericity: The cache should be able to store any type of value.
- Concurrency Safety: The cache implementations should be thread-safe, allowing concurrent access from multiple goroutines. Use appropriate locking mechanisms (e.g.,
sync.Mutex). - Capacity Limit: Each cache should have a maximum capacity.
- Eviction Strategies:
- LRU: Evicts the least recently used item.
- FIFO: Evicts the oldest item (first one added).
- LFU: Evicts the least frequently used item.
- Return Values:
Getshould return the value associated with the key and a boolean indicating whether the key exists in the cache.
Expected Behavior:
Putshould add a new key-value pair to the cache. If the cache is full, it should evict an item based on the chosen eviction strategy before adding the new pair.Getshould return the value associated with the key if it exists. If the key doesn't exist, it should returnnilandfalse.Deleteshould remove the key-value pair associated with the key.
Edge Cases to Consider:
- Cache capacity of 0.
- Concurrent access from multiple goroutines.
- Retrieving a non-existent key.
- Adding duplicate keys (should overwrite the existing value).
- Deleting a non-existent key (should be a no-op).
Examples
Example 1: LRU Cache
Input: capacity = 2, operations = [Put("A", 1), Put("B", 2), Get("A"), Put("C", 3), Get("B")]
Output: [1, nil, 2] (representing the return values of Get("A") and Get("B"))
Explanation: The LRU cache initially stores "A" and "B". Getting "A" makes it the most recently used. Adding "C" evicts "B" because it was the least recently used. Getting "B" returns nil because it was evicted.
Example 2: FIFO Cache
Input: capacity = 2, operations = [Put("A", 1), Put("B", 2), Get("A"), Put("C", 3), Get("B")]
Output: [1, nil]
Explanation: The FIFO cache initially stores "A" and "B". Getting "A" doesn't affect the order. Adding "C" evicts "A" because it was the first one added. Getting "B" returns nil because "A" was evicted.
Example 3: LFU Cache
Input: capacity = 2, operations = [Put("A", 1), Get("A"), Put("B", 2), Get("A"), Get("B"), Put("C", 3)]
Output: [1, 1, 2, nil]
Explanation: The LFU cache initially stores "A" with a frequency of 1 and "B" with a frequency of 1. Getting "A" increases its frequency to 2. Adding "C" evicts "B" because it has a frequency of 1, which is lower than "A"'s frequency of 2.
Constraints
- Cache capacity will be a positive integer.
- Keys will be strings.
- Values can be of any type (interface{}).
- The number of operations will be within a reasonable range (e.g., less than 1000).
- The implementation should be reasonably efficient. While absolute performance isn't the primary focus, avoid excessively inefficient data structures or algorithms. O(1) or O(log n) for
Get,Put, andDeleteare desirable.
Notes
- Consider using appropriate data structures like linked lists, maps, and counters to implement the eviction strategies efficiently.
- Think carefully about how to handle concurrency safely. Use mutexes to protect shared data structures.
- The
Cacheinterface provides a clear contract for your cache implementations. - Focus on correctness and clarity of code. Well-commented code is appreciated.
- You can assume that the input operations are valid (e.g., keys are not nil).
- You don't need to implement persistence (saving the cache to disk). The cache should reside in memory.
- Consider using a struct to hold the cache data and the mutex.