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: