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.