Sling Academy
Home/Golang/Implementing a Cache System Using Maps in Go

Implementing a Cache System Using Maps in Go

Last updated: November 24, 2024

In this article, we'll explore how to implement a cache system using maps in the Go programming language. A cache is a storage layer that temporarily stores data to serve requests faster. Caching is particularly beneficial when retrieving data from a slow backend system multiple times.

Basic Implementation

Let's start by implementing a simple cache using a Go map. This basic version will allow us to store and retrieve items by key.

package main

import (
    "fmt"
)

type Cache struct {
    items map[string]string
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]string),
    }
}

func (c *Cache) Set(key, value string) {
    c.items[key] = value
}

func (c *Cache) Get(key string) (string, bool) {
    value, found := c.items[key]
    return value, found
}

func main() {
    cache := NewCache()
    cache.Set("key1", "value1")
    if value, found := cache.Get("key1"); found {
        fmt.Println("Fetched from cache:", value)
    } else {
        fmt.Println("Key not found in cache")
    }
}

This simple cache allows us to store values and retrieve them using corresponding keys. If a requested key doesn’t exist, we return a false boolean.

Intermediate Implementation: Expiry Feature

Let's enhance our cache with an expiry feature. We will set a lifespan for each cached item, after which it should be considered expired.

package main

import (
    "fmt"
    "time"
)

type CacheItem struct {
    value      string
    expiryTime time.Time
}

type Cache struct {
    items map[string]CacheItem
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]CacheItem),
    }
}

func (c *Cache) Set(key, value string, duration time.Duration) {
    c.items[key] = CacheItem{
        value:      value,
        expiryTime: time.Now().Add(duration),
    }
}

func (c *Cache) Get(key string) (string, bool) {
    item, found := c.items[key]
    if !found || item.expiryTime.Before(time.Now()) {
        return "", false
    }
    return item.value, true
}

func main() {
    cache := NewCache()
    cache.Set("key1", "value1", 5*time.Second)
    time.Sleep(6 * time.Second)
    if value, found := cache.Get("key1"); found {
        fmt.Println("Fetched from cache:", value)
    } else {
        fmt.Println("Key not found in cache or expired")
    }
}

In this version, each cache entry expires after a certain period. This is implemented by comparing the current time with the expiry time of each cache entry during retrieval.

Advanced Implementation: Thread-Safe Cache

In concurrent environments, data race conditions can occur when accessing shared resources such as our cache. To make it thread-safe, we can use Go’s synchronization mechanisms.

package main

import (
    "fmt"
    "sync"
    "time"
)

type CacheItem struct {
    value      string
    expiryTime time.Time
}

type Cache struct {
    items map[string]CacheItem
    mu    sync.RWMutex
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]CacheItem),
    }
}

func (c *Cache) Set(key, value string, duration time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = CacheItem{
        value:      value,
        expiryTime: time.Now().Add(duration),
    }
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    item, found := c.items[key]
    if !found || item.expiryTime.Before(time.Now()) {
        return "", false
    }
    return item.value, true
}

func main() {
    cache := NewCache()
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", i)
            cache.Set(key, fmt.Sprintf("value%d", i), 2*time.Second)
            if value, found := cache.Get(key); found {
                fmt.Printf("Fetched from cache: %s = %s\n", key, value)
            } else {
                fmt.Println("Key not found in cache or expired")
            }
        }(i)
    }
    wg.Wait()
}

This version of the cache makes use of a sync.RWMutex to ensure safe concurrent read/write access. Users can write to or read from the cache in a thread-safe manner, protecting against data races in a multi-goroutine context.

Each implementation progressively introduces advanced concepts like item expiration and thread safety to cater to different caching needs in a Go application.

Next Article: Using Maps to Build Frequency Histograms in Go

Previous Article: Handling Default Values in Go Maps Using Helper Functions

Series: Working with Maps in Go

Golang

Related Articles

You May Also Like

  • How to remove HTML tags in a string in Go
  • How to remove special characters in a string in Go
  • How to remove consecutive whitespace in a string in Go
  • How to count words and characters in a string in Go
  • Relative imports in Go: Tutorial & Examples
  • How to run Python code with Go
  • How to generate slug from title in Go
  • How to create an XML sitemap in Go
  • How to redirect in Go (301, 302, etc)
  • Using Go with MongoDB: CRUD example
  • Auto deploy Go apps with CI/ CD and GitHub Actions
  • Fixing Go error: method redeclared with different receiver type
  • Fixing Go error: copy argument must have slice type
  • Fixing Go error: attempted to use nil slice
  • Fixing Go error: assignment to constant variable
  • Fixing Go error: cannot compare X (type Y) with Z (type W)
  • Fixing Go error: method has pointer receiver, not called with pointer
  • Fixing Go error: assignment mismatch: X variables but Y values
  • Fixing Go error: array index must be non-negative integer constant