Implement an in-memory cache in Golang
Tags: go, inmemory, cache
Published: 2022-01-17 15:41:15

# Overview
This article is a translation of [Golangでインメモリなキャッシュを実装する](https://bmf-tech.com/posts/Golang%E3%81%A7%E3%82%A4%E3%83%B3%E3%83%A1%E3%83%A2%E3%83%AA%E3%81%AA%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B).
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] (https://github.com/bmf-san/go-snippets/blob/master/architecture_design/cache/cache.go])

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

```golang
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))
}
```

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] (https://github.com/bmf-san/go-snippets/blob/master/architecture_design/cache/cache_with_goroutine.go)

An implementation version that meets the requirements.

```golang
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)
}
```

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
- [github.com - patrickmn/go-cache](https://github.com/patrickmn/go-cache)
- [Stack overflow - Map with TTL option in Go](https://stackoverflow.com/questions/25484122/map-with-ttl-option-in-go)
- [groups.google.com - sync.Map for caching](https://groups.google.com/g/golang-nuts/c/avSIKqUKKAM?pli=1)
- [golang.org - pkg/sync/#Map](https://golang.org/pkg/sync/#Map)