DEV Community

Kenta Takeuchi
Kenta Takeuchi

Posted on • Originally published at bmf-tech.com

Implement an in-memory cache in Golang

Overview

This article is a translation of Golangでインメモリなキャッシュを実装する.
Golang's in-memory cache library looks good, but it's lightweight and simple enough, so I implemented it myself.

Implementation

Requirements

  • Can hold multiple data.
  • You can keep time-limited data in memory. It should be destroyed from memory when the deadline is reached.
  • Be aware of data lock in consideration of simultaneous reference and update to cache.

Initial design

cf. Github.com - bmf-san/go-snippets/architecture_design/cache/cache.go

I implemented it as if I first came up with it.

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 that sync.Map, which does not have to worry about lock processing, is convenient, but I rejected it because it does not meet the requirements in terms of data structure and functionality.

Release version

cf. Github.com - bmf-san/go-snippets/architecture_design/cache/cache.go

An implementation version that meets the requirements.

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 if I store cache data in sync.Map, it is difficult to check and delete the cache data without specifying the cache key, so map is used to retain the cache data. I decided to adopt.

Expiration check is done at intervals using ticker.
In the above, the interval is every second.
In this implementation, the cache can be accessed until the cache expiration time + 1 second has elapsed, so the actual cache expiration date is the time specified by expires plus the interval.

Impressions

It was a good time to get started with concurrency and locking in Golang.

Reference

Discussion (0)