Implementing a Time-Based Cache Invalidation Strategy in Go
Caching is a fundamental technique for improving application performance by storing frequently accessed data in memory. However, stale data can lead to incorrect results. This challenge requires you to implement a time-based cache invalidation mechanism for a simple key-value cache in Go, ensuring that cached data is considered invalid and re-fetched after a specified duration.
Problem Description
You need to create a Cache struct in Go that stores key-value pairs. Each entry in the cache should have an associated expiration time. The Cache should provide the following functionalities:
NewCache(defaultTTL time.Duration): A constructor function to create a new cache instance. It should accept adefaultTTL(Time To Live) which will be used for new cache entries if not specified otherwise.Set(key string, value interface{}, ttl time.Duration): Adds or updates a key-value pair in the cache. Ifttlis0, thedefaultTTLwill be used. The entry should be marked with an expiration timestamp calculated from the current time plus the specifiedttl.Get(key string): Retrieves the value associated with a givenkey. If the key exists and has not expired, its value should be returned. If the key does not exist or has expired, it should returnnilandfalse.Delete(key string): Removes a key-value pair from the cache.Has(key string): Checks if a key exists in the cache and has not expired. Returnstrueif valid,falseotherwise.
The cache should implicitly invalidate expired entries when Get or Has is called. There is no need for a separate background cleanup goroutine for this specific challenge.
Examples
Example 1:
package main
import (
"fmt"
"time"
)
// Assuming Cache struct and methods are defined elsewhere
func main() {
cache := NewCache(2 * time.Second)
cache.Set("user:1", "Alice", 0) // Uses default TTL of 2 seconds
cache.Set("product:101", 12345, 5 * time.Second)
value, ok := cache.Get("user:1")
fmt.Printf("user:1: Value: %v, Found: %v\n", value, ok) // Expected: Value: Alice, Found: true
time.Sleep(3 * time.Second)
value, ok = cache.Get("user:1")
fmt.Printf("user:1 (after 3s): Value: %v, Found: %v\n", value, ok) // Expected: Value: <nil>, Found: false
value, ok = cache.Get("product:101")
fmt.Printf("product:101: Value: %v, Found: %v\n", value, ok) // Expected: Value: 12345, Found: true
}
Output:
user:1: Value: Alice, Found: true
user:1 (after 3s): Value: <nil>, Found: false
product:101: Value: 12345, Found: true
Explanation: The "user:1" entry expires after 2 seconds (default TTL). When Get is called after 3 seconds, it's considered expired and returns nil, false. The "product:101" entry, with a TTL of 5 seconds, is still valid.
Example 2:
package main
import (
"fmt"
"time"
)
// Assuming Cache struct and methods are defined elsewhere
func main() {
cache := NewCache(1 * time.Second)
cache.Set("temp_data", []int{1, 2, 3}, 3 * time.Second)
if cache.Has("temp_data") {
fmt.Println("temp_data is in cache.") // Expected: temp_data is in cache.
}
cache.Delete("temp_data")
if !cache.Has("temp_data") {
fmt.Println("temp_data has been deleted.") // Expected: temp_data has been deleted.
}
}
Output:
temp_data is in cache.
temp_data has been deleted.
Explanation: The "temp_data" is added and Has correctly reports its presence. After Delete is called, Has returns false.
Example 3: (Edge case: Setting with a very short TTL and checking immediately)
package main
import (
"fmt"
"time"
)
// Assuming Cache struct and methods are defined elsewhere
func main() {
cache := NewCache(1 * time.Minute)
cache.Set("short_lived", "data", 1*time.Millisecond)
// Give a tiny moment for the system clock to tick if needed, though typically not required for this scale
time.Sleep(2 * time.Millisecond)
value, ok := cache.Get("short_lived")
fmt.Printf("short_lived: Value: %v, Found: %v\n", value, ok) // Expected: Value: <nil>, Found: false
}
Output:
short_lived: Value: <nil>, Found: false
Explanation: Although set with a very short TTL, the explicit time.Sleep ensures that the expiration check after a minimal delay would correctly identify it as expired.
Constraints
- The cache should be thread-safe. Multiple goroutines may access the cache concurrently.
- The
valuestored in the cache can be of any type (interface{}). - The
defaultTTLand individualttldurations will be non-negative. - Keys are non-empty strings.
Notes
- Consider using
time.Now().Add(ttl)to calculate expiration timestamps. - A
sync.RWMutexis a good choice for managing concurrent access to the cache data structure. - You'll need a way to store the key, the value, and its expiration timestamp for each cache entry.
- When checking for expiration, compare the current time with the stored expiration timestamp.
- For
GetandHasoperations, if an entry is found to be expired, it should be treated as if it doesn't exist and optionally removed from the cache's internal storage.