Imcache
A zero-dependency generic in-memory cache Go library
Install / Use
/learn @erni27/ImcacheREADME
imcache
imcache is a zero-dependency generic in-memory cache Go library.
It supports absolute expiration, sliding expiration, max entries limit, eviction callbacks and sharding. It's safe for concurrent use by multiple goroutines.
import "github.com/erni27/imcache"
Usage
package main
import (
"fmt"
"github.com/erni27/imcache"
)
func main() {
// Zero value Cache is a valid non-sharded cache
// with no expiration, no sliding expiration,
// no entry limit and no eviction callback.
var c imcache.Cache[uint32, string]
c.Set(1, "one", imcache.WithNoExpiration())
value, ok := c.Get(1)
if !ok {
panic("value for the key '1' not found")
}
fmt.Println(value)
}
Expiration
imcache supports the following expiration options:
WithNoExpiration- the entry will never expire.WithExpiration- the entry will expire after a certain time.WithExpirationDate- the entry will expire at a certain date.WithSlidingExpiration- the entry will expire after a certain time if it hasn't been accessed. The expiration time is reset every time the entry is accessed. It is slided to now + sliding expiration time when now is the time when the entry was accessed.
// The entry will never expire.
c.Set(1, "one", imcache.WithNoExpiration())
// The entry will expire after 1 second.
c.Set(2, "two", imcache.WithExpiration(time.Second))
// The entry will expire at the given date.
c.Set(3, "three", imcache.WithExpirationDate(time.Now().Add(time.Second)))
// The entry will expire after 1 second if it hasn't been accessed.
// Otherwise, the expiration time will slide to the access time + 1 second.
c.Set(4, "four", imcache.WithSlidingExpiration(time.Second))
One can also use the WithExpirationOption and WithSlidingExpirationOption options to set the default expiration time for the given cache instance. By default, the default expiration time is set to no expiration.
// Create a new cache instance with the default expiration time equal to 1 second.
c1 := imcache.New[int32, string](imcache.WithDefaultExpirationOption[int32, string](time.Second))
// The entry will expire after 1 second (the default expiration time).
c1.Set(1, "one", imcache.WithDefaultExpiration())
// Create a new cache instance with the default sliding expiration time equal to 1 second.
c2 := imcache.New[int32, string](imcache.WithDefaultSlidingExpirationOption[int32, string](time.Second))
// The entry will expire after 1 second (the default expiration time) if it hasn't been accessed.
// Otherwise, the expiration time will slide to the access time + 1 second.
c2.Set(1, "one", imcache.WithDefaultExpiration())
Key eviction
imcache actively evicts expired entries. It removes expired entries when they are accessed by most of Cache methods (both read and write). Peek, PeekMultiple and PeekAll methods are the exception. They don't remove the expired entries and do not slide the expiration time (if the sliding expiration is set).
It is possible to use the Cleaner to periodically remove expired entries from the cache. The Cleaner is a background goroutine that periodically removes expired entries from the cache. The Cleaner is disabled by default. One can use the WithCleanerOption option to enable the Cleaner and set the cleaning interval.
// Create a new Cache with a Cleaner which will remove expired entries every 5 minutes.
c := imcache.New[string, string](imcache.WithCleanerOption[string, string](5 * time.Minute))
// Close the Cache. This will stop the Cleaner if it is running.
defer c.Close()
To be notified when the entry is evicted from the cache, one can use the EvictionCallback. It's a function that accepts the key and the value of the evicted entry along with the reason why the entry was evicted. One can use the WithEvictionCallbackOption option to set the EvictionCallback for the given cache instance.
package main
import (
"log"
"time"
"github.com/erni27/imcache"
)
func LogEvictedEntry(key string, value interface{}, reason imcache.EvictionReason) {
log.Printf("Evicted entry: %s=%v (%s)", key, value, reason)
}
func main() {
c := imcache.New[string, interface{}](
imcache.WithDefaultExpirationOption[string, interface{}](time.Second),
imcache.WithEvictionCallbackOption[string, interface{}](LogEvictedEntry),
)
c.Set("foo", "bar", imcache.WithDefaultExpiration())
time.Sleep(time.Second)
_, ok := c.Get("foo")
if ok {
panic("expected entry to be expired")
}
}
EvictionCallback is invoked in a separate goroutine to not block any Cache method.
Max entries limit
imcache supports setting the max entries limit. When the max entries limit is reached, the entry is evicted according to the chosen eviction policy. imcache supports the following eviction policies:
EvictionPolicyLRU- the least recently used entry is evicted.EvictionPolicyLFU- the least frequently used entry is evicted.EvictionPolicyRandom- a random entry is evicted.
One can use the WithMaxEntriesLimitOption option to set the max entries limit and the eviction policy for the given cache instance.
c := imcache.New[uint32, string](imcache.WithMaxEntriesLimitOption[uint32, string](1000, imcache.EvictionPolicyLRU))
Sharding
imcache supports sharding. Each shard is a separate Cache instance. A shard for a given key is selected by computing the hash of the key and taking the modulus of the number of shards. imcache exposes the Hasher64 interface that wraps Sum64 accepting a key and returning a 64-bit hash of the input key. It can be used to implement custom sharding algorithms.
A Sharded instance can be created by calling the NewSharded method.
c := imcache.NewSharded[string, string](4, imcache.DefaultStringHasher64{})
All previous examples apply to Sharded type as well. Note that Option(s) are applied to each shard (Cache instance separately) not to the Sharded instance itself.
Performance
imcache was compared to the vanilla Go map with simple locking mechanism. The benchmarks were run on an Apple M1 Pro 8-core CPU with 32 GB of RAM running macOS Ventura 13.4.1 using Go 1.21.6.
Reads
go version
go version go1.21.6 darwin/arm64
go test -benchmem -bench "Get_|Get$|Peek_|Peek$"
goos: darwin
goarch: arm64
pkg: github.com/erni27/imcache
BenchmarkCache_Get-8 2655246 428.5 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/2_Shards-8 2810713 436.8 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/4_Shards-8 2732820 444.9 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/8_Shards-8 2957444 445.7 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/16_Shards-8 2773999 447.0 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/32_Shards-8 2752075 443.4 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/64_Shards-8 2752899 439.7 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get/128_Shards-8 2771691 456.3 ns/op 23 B/op 1 allocs/op
BenchmarkCache_Get_MaxEntriesLimit_EvictionPolicyLRU-8 2410712 526.8 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/2_Shards-8 2346715 543.2 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/4_Shards-8 2317453 566.2 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/8_Shards-8 2293774 556.5 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/16_Shards-8 2292554 557.7 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/32_Shards-8 2262634 542.0 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/64_Shards-8 2318079 544.2 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLRU/128_Shards-8 2278434 565.6 ns/op 23 B/op 1 allocs/op
BenchmarkCache_Get_MaxEntriesLimit_EvictionPolicyLFU-8 2482602 528.0 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLFU/2_Shards-8 2403782 534.2 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLFU/4_Shards-8 2286364 548.8 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_EvictionPolicyLFU/8_Shards-8 2239857 576.0 ns/op 23 B/op 1 allocs/op
BenchmarkSharded_Get_MaxEntriesLimit_Evicti
