Golang In-memory Cache
Golang In-memory Cache Implementation
I'm currently building a simple Go web app and wanted to optimize the performance by introducing an in-memory cache. Caching is crucial for improving the speed of data retrieval operations, reducing the load on databases, and enhancing the overall user experience. In this blog, I will walk through the process of creating a basic in-memory cache in Golang, which can be used to store key-value pairs.
Create a New Go Module
Create a cache
package in a new Go module named cache
.
internal
├── your go package
│ └── your go file
└── cache
└── cache.go <- Create this file
Step 1: Define the Cache Structure and the New()
function
package cache
import (
"sync"
"time"
)
type item[V any] struct {
value V
expiration time.Time
} // Single Item type
type Cache[K comparable, V any] struct {
items map[K]item[V] // map of items
mu sync.Mutex
} // Cache type
In the above code snippet, we define two custom types: item
and Cache
.
The item
type represents a single key-value pair along with the expiration time. The item
starts with a lowercase letter, which makes it private to the package.
The Cache
type is a collection of items stored in a map with a mutex to handle concurrent access.
func (i item[V]) isExpired() bool {
return i.expiration.Before(time.Now())
}
func New[K comparable, V any]() *Cache[K, V] {
c := &Cache[K, V]{
items: make(map[K]item[V]),
}
go func() {
for range time.Tick(24 * time.Hour) { // Check every day
c.mu.Lock()
for k, v := range c.items {
if v.isExpired() {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}()
return c
}
In the above code snippet, we define a New()
function that creates a new cache instance.
The function initializes the cache with an empty map and starts a goroutine to periodically check for expired items and remove them from the cache.
Note: The sync.Mutex
in the Cache
struct ensures that access to the items
map is thread-safe.
The Lock()
and Unlock()
methods are used to prevent race conditions when multiple goroutines attempt to read from or write to the cache concurrently.
This is essential for maintaining data integrity in a concurrent environment.
Step 2: Implement the Set()
& Get()
methods
Set()
method is used to add or update a key-value pair in the cache with a specified time-to-live (TTL).Get()
method is used to retrieve a value from the cache based on the key.
func (c *Cache[K, V]) Set(key K, val V, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = item[V]{
value: val,
expiration: time.Now().Add(ttl),
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
item, ok := c.items[key]
if !ok {
return item.value, false // Zero value
}
if item.isExpired() {
delete(c.items, key)
return item.value, false
}
return item.value, true
}
Note: Remember to use
c.mu.Lock()
to acquire the lock before accessing theitems
map anddefer c.mu.Unlock()
to release the lock when done.
Step 3: Implement the Delete()
& Pop()
methods
Delete()
method is used to remove a key-value pair from the cache.Pop()
method is used to retrieve and remove a key-value pair from the cache based on the key.
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache[K, V]) Pop(key K) (V, bool) {
val, has := c.Get(key)
if !has {
return val, has
}
c.Delete(key)
return val, has
}
Test the In-memory Cache
Create a test file named cache_test.go
in the cache
package to write unit tests for the cache implementation.
internal
├── your go package
│ └── your go file
└── cache
├── cache.go
└── cache_test.go <- Create this file
Step 1: Import the Required Packages
package cache
import (
"testing"
"time"
)
Step 2: Create Test Cases
func TestCache(t *testing.T) {
type Value struct {
Name string
}
c := New[string, Value]() // Create a new cache with string keys and Value values
// Add 2 key value pairs of TTL 30 seconds
c.Set("chaotingchen10@gmail.com", Value{Name: "Tim"}, time.Second*30)
c.Set("dev@dev.com", Value{Name: "dev"}, time.Second*30)
c.Pop("dev@dev.com") // First pop the key value pair with key "dev@dev.com"
v, ok := c.Get("dev@dev.com") // Check if the key value pair is deleted
if ok {
t.Error("(Error) expected value to be deleted")
} else {
t.Log("(Success) Value poped")
}
v, ok = c.Get("chaotingchen10@gmail.com") // Get a key value pair which isn't deleted
if !ok {
t.Error("(Error) expected value to be found")
} else {
t.Log("(Success) Found: ", v)
}
time.Sleep(time.Second * 35) // Wait for 35 seconds to expire the key value pair
v, ok = c.Get("chaotingchen10@gmail.com")
if ok {
t.Error("(Error) expected value to be expired")
} else {
t.Log("(Success) Expired")
}
}
Step 3: Run the Test
Run go test
in the cache
package directory to execute the test cases.
go test
To print the logs, run the test with the -v
flag, i.e., go test -v
.
Conclusion
Although their are many ways to implement caching, including using Redis, Memcached, or other third-party services, an in-memory cache can be a simple and effective solution for small-scale applications. Hope you found this blog helpful in understanding how to create an in-memory cache in Golang.
Happy coding!!