DEV Community

Cover image for Implementing In-Memory Cache in Golang
Kenta Takeuchi
Kenta Takeuchi

Posted on • Originally published at bmf-tech.com

Implementing In-Memory Cache in Golang

This article was originally published on bmf-tech.com.

Overview

There are some good libraries for in-memory caching in Golang, but since I needed something lightweight and simple, I decided to implement my own.

Implementation

Requirements

  • Can hold multiple data items.
  • Can hold data in memory with an expiration time. The data should be discarded from memory once the expiration time is reached.
  • Consider simultaneous access and updates to the cache, with awareness of data locking.

Initial Design

The initial implementation was done based on my first thoughts.

package main

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

// Cache is a struct for caching.
type Cache struct {
    value   sync.Map
    expires int64
}

// Expired determines if it has expired.
func (c *Cache) Expired(time int64) bool {
    if c.expires == 0 {
        return false
    }
    return time > c.expires
}

// Get gets a value from a cache. Returns an empty string if the value does not exist or has expired.
func (c *Cache) Get(key string) string {
    if c.Expired(time.Now().UnixNano()) {
        log.Printf("%s has expired", key)
        return ""
    }
    v, ok := c.value.Load(key)
    var s string
    if ok {
        s, ok = v.(string)
        if !ok {
            log.Printf("%s does not exists", key)
            return ""
        }
    }
    return s
}

// Put puts a value to a cache. If a key and value exists, overwrite it.
func (c *Cache) Put(key string, value string, expired int64) {
    c.value.Store(key, value)
    c.expires = expired
}

var cache = &Cache{}

func main() {
    fk := "first-key"
    sk := "second-key"

    cache.Put(fk, "first-value", time.Now().Add(2*time.Second).UnixNano())
    s := cache.Get(fk)
    fmt.Println(cache.Get(fk))

    time.Sleep(5 * time.Second)

    // fk should have expired
    s = cache.Get(fk)
    if len(s) == 0 {
        cache.Put(sk, "second-value", time.Now().Add(100*time.Second).UnixNano())
    }
    fmt.Println(cache.Get(sk))
}
Enter fullscreen mode Exit fullscreen mode

I thought sync.Map was convenient because I didn't have to worry about locking, but it was rejected because it did not meet the requirements in terms of data structure and functionality.

Release Version

The version that meets the requirements is available at github.com - bmf-san/go-snippets/architecture_design/cache/cache_with_goroutine.go.

package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)

// item is the data to be cached.
type item struct {
    value   string
    expires int64
}

// Cache is a struct for caching.
type Cache struct {
    items map[string]*item
    mu    sync.Mutex
}

func New() *Cache {
    c := &Cache{items: make(map[string]*item)}
    go func() {
        t := time.NewTicker(time.Second)
        defer t.Stop()
        for {
            select {
            case <-t.C:
                c.mu.Lock()
                for k, v := range c.items {
                    if v.Expired(time.Now().UnixNano()) {
                        log.Printf("%v has expires at %d", c.items, time.Now().UnixNano())
                        delete(c.items, k)
                    }
                }
                c.mu.Unlock()
            }
        }
    }()
    return c
}

// Expired determines if it has expires.
func (i *item) Expired(time int64) bool {
    if i.expires == 0 {
        return true
    }
    return time > i.expires
}

// Get gets a value from a cache.
func (c *Cache) Get(key string) string {
    c.mu.Lock()
    var s string
    if v, ok := c.items[key]; ok {
        s = v.value
    }
    c.mu.Unlock()
    return s
}

// Put puts a value to a cache. If a key and value exists, overwrite it.
func (c *Cache) Put(key string, value string, expires int64) {
    c.mu.Lock()
    if _, ok := c.items[key]; !ok {
        c.items[key] = &item{
            value:   value,
            expires: expires,
        }
    }
    c.mu.Unlock()
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fk := "first-key"
        sk := "second-key"

        cache := New()

        cache.Put(fk, "first-value", time.Now().Add(2*time.Second).UnixNano())
        fmt.Println(cache.Get(fk))

        time.Sleep(10 * time.Second)

        if len(cache.Get(fk)) == 0 {
            cache.Put(sk, "second-value", time.Now().Add(100*time.Second).UnixNano())
        }
        fmt.Println(cache.Get(sk))
    })
    http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

I wanted to use sync.Map because it is convenient, but it was difficult to check and delete expired cache data without specifying the cache key. Therefore, I decided to use map to hold the cache data.

The expiration check is done using a ticker to check at intervals. In the above implementation, the interval is set to one second. In this implementation, access to the cache can occur until one second after the cache expiration, so the actual cache expiration is the time specified in expires plus the interval.

Thoughts

This was a good opportunity to learn about concurrency and locking in Golang.

References

Top comments (0)