Hone logo
Hone
Problems

Implementing a Response Cache in Go

Response caching is a crucial technique for optimizing applications that frequently make the same requests. This challenge asks you to implement a simple in-memory response cache in Go, allowing you to store and retrieve responses to reduce latency and load on backend services. This is particularly useful for APIs and services where certain requests are repeated often.

Problem Description

You are tasked with creating a Cache struct in Go that can store and retrieve responses based on a key. The cache should support the following operations:

  • Get(key string) (interface{}, bool): Retrieves a response from the cache based on the provided key. Returns the response (interface{}) and a boolean indicating whether the key was found in the cache. If the key is not found, return nil, false.
  • Set(key string, response interface{}): Stores a response in the cache associated with the provided key.
  • Expire(key string, duration time.Duration): Sets an expiration time for a specific key. After the duration has passed, the key will be automatically removed from the cache.
  • Delete(key string): Removes a key and its associated response from the cache.

The cache should use time.Duration to manage expiration times. You'll need to implement a mechanism to periodically check for expired entries and remove them. The cache should be thread-safe.

Key Requirements:

  • Thread Safety: The cache operations must be safe for concurrent access from multiple goroutines. Use appropriate synchronization primitives (e.g., sync.Mutex).
  • Expiration: Implement expiration functionality using time.Duration.
  • Interface{}: Store responses as interface{} to allow caching of various data types.
  • Error Handling: While this challenge doesn't require explicit error returns, ensure your code handles potential panics gracefully.

Expected Behavior:

  • Get should return the cached response if the key exists and hasn't expired.
  • Set should store the response associated with the key.
  • Expire should set the expiration time for the key.
  • Delete should remove the key and its associated response.
  • Expired entries should be automatically removed from the cache.

Edge Cases to Consider:

  • Concurrent access to the cache (multiple goroutines calling Get, Set, Expire, and Delete simultaneously).
  • Expiration of entries.
  • Handling of nil responses.
  • Empty cache.

Examples

Example 1:

Input:
  cache := NewCache(5 * time.Second) // Cache expires after 5 seconds
  cache.Set("key1", "value1")
  response, ok := cache.Get("key1")
  fmt.Println(response, ok)
  time.Sleep(6 * time.Second)
  response, ok = cache.Get("key1")
  fmt.Println(response, ok)
Output:
value1 true
<nil> false
Explanation: The first `Get` retrieves the cached value. After 6 seconds (longer than the expiration time), the second `Get` returns `nil` and `false` because the entry has expired.

Example 2:

Input:
  cache := NewCache(0) // No expiration
  cache.Set("key2", 123)
  response, ok := cache.Get("key2")
  fmt.Println(response, ok)
  cache.Delete("key2")
  response, ok = cache.Get("key2")
  fmt.Println(response, ok)
Output:
123 true
<nil> false
Explanation: The first `Get` retrieves the cached value. `Delete` removes the entry, so the second `Get` returns `nil` and `false`.

Example 3: (Concurrent Access)

Input: (Multiple goroutines accessing the cache)
  cache := NewCache(10 * time.Second)
  cache.Set("key3", "initial_value")

  go func() {
    time.Sleep(2 * time.Second)
    response, ok := cache.Get("key3")
    fmt.Println("Goroutine 1:", response, ok)
  }()

  go func() {
    time.Sleep(3 * time.Second)
    cache.Set("key3", "updated_value")
    response, ok := cache.Get("key3")
    fmt.Println("Goroutine 2:", response, ok)
  }()

  time.Sleep(5 * time.Second)
Output: (Order may vary due to concurrency)
Goroutine 1: initial_value true
Goroutine 2: updated_value true
Explanation:  Demonstrates concurrent access.  The first goroutine retrieves the initial value. The second goroutine updates the value and then retrieves it.  The order of the prints is not guaranteed.

Constraints

  • Cache Size: The cache is unbounded (no maximum size).
  • Expiration Time: Expiration times are represented as time.Duration.
  • Concurrency: The cache must handle concurrent access from multiple goroutines.
  • Response Type: Responses are stored as interface{}.
  • Time Resolution: The cache's expiration check should run at least once every second.

Notes

  • Consider using a sync.Mutex to protect the cache's internal data structures from concurrent access.
  • You can use a goroutine with a time.Ticker to periodically check for expired entries.
  • The NewCache function should initialize the cache and start the expiration cleanup goroutine.
  • Focus on correctness and thread safety. Performance optimization is secondary for this challenge.
  • Think about how to handle potential race conditions when setting expiration times and deleting entries.
Loading editor...
go