
Implementing a Simple Caching Layer in Go
Overview of Caching
Caching is a technique used to store a subset of data that is expensive to fetch or compute. By keeping frequently accessed data in memory, applications can reduce latency and improve response times. In this tutorial, we will build a basic cache with the following features:
- Key-value storage
- Expiration of cached items
- Thread-safe access
Basic Cache Structure
We will start by defining a structure for our cache. The cache will hold a map of items and a mutex for safe concurrent access. Additionally, we will include expiration timestamps for each item.
package main
import (
"sync"
"time"
)
type CacheItem struct {
Value interface{}
Expiration int64
}
type Cache struct {
items map[string]CacheItem
mu sync.RWMutex
}
// NewCache initializes a new Cache instance.
func NewCache() *Cache {
return &Cache{
items: make(map[string]CacheItem),
}
}Adding Items to the Cache
Next, we will implement a method to add items to the cache. We will also include an optional expiration time. If the expiration time is not provided, the item will not expire.
// Set adds a new item to the cache with an optional expiration.
func (c *Cache) Set(key string, value interface{}, duration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
expiration := int64(0)
if duration > 0 {
expiration = time.Now().Add(duration).UnixNano()
}
c.items[key] = CacheItem{
Value: value,
Expiration: expiration,
}
}Retrieving Items from the Cache
We will also need a method to retrieve items. This method will check if the item exists and whether it has expired.
// Get retrieves an item from the cache.
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[key]
if !found {
return nil, false
}
// Check if the item has expired
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
// Remove expired item
delete(c.items, key)
return nil, false
}
return item.Value, true
}Deleting Items from the Cache
To manage the cache effectively, we should also implement a method to delete items.
// Delete removes an item from the cache.
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}Example Usage
Now that we have our cache implemented, let’s see how to use it in a simple example.
func main() {
cache := NewCache()
// Set items in the cache
cache.Set("foo", "bar", 5*time.Second)
cache.Set("baz", 42, 0) // This item will not expire
// Retrieve items
value, found := cache.Get("foo")
if found {
fmt.Println("Found foo:", value)
} else {
fmt.Println("foo not found or expired")
}
// Wait for expiration
time.Sleep(6 * time.Second)
value, found = cache.Get("foo")
if found {
fmt.Println("Found foo:", value)
} else {
fmt.Println("foo not found or expired")
}
}Testing the Cache
To ensure our cache works as expected, we should write some tests. Below is a simple test suite that checks the core functionalities of our cache.
package main
import (
"testing"
"time"
)
func TestCache(t *testing.T) {
cache := NewCache()
cache.Set("test", "value", 1*time.Second)
value, found := cache.Get("test")
if !found || value != "value" {
t.Errorf("Expected to find 'test', got %v", value)
}
time.Sleep(2 * time.Second)
_, found = cache.Get("test")
if found {
t.Error("Expected 'test' to be expired")
}
cache.Set("permanent", "data", 0)
value, found = cache.Get("permanent")
if !found || value != "data" {
t.Errorf("Expected to find 'permanent', got %v", value)
}
}Conclusion
In this tutorial, we have implemented a simple in-memory caching layer in Go. We covered fundamental operations such as setting, getting, and deleting items while ensuring thread safety. This caching mechanism can be expanded further by adding features such as size limits, eviction policies, or support for distributed caching.
Learn more with useful resources:
