feat: use generics

This commit is contained in:
Bruno Carlin 2025-01-18 00:55:00 +01:00
parent cb38484486
commit d61b3d6865
Signed by: bcarlin
GPG key ID: 8E254EA0FFEB9B6D
5 changed files with 2172 additions and 669 deletions

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,8 @@
//
// It supports exîration dates and can store arbitrary values of any type.
// Keys must be strings.
//
// Cache values are safe to share between goroutines.
package cache
import (
@ -9,75 +11,83 @@ import (
"time"
)
type entry struct {
type entry[V any] struct {
expirationDate *time.Time
value interface{}
value V
}
func (e entry[V]) isExpired() bool {
return e.expirationDate != nil && e.expirationDate.Before(time.Now())
}
// Cache is an in-memory key-value store.
type Cache struct {
data map[string]entry
type Cache[K comparable, V any] struct {
data map[K]entry[V]
mu *sync.RWMutex
}
// New instantiate a new cache.
func New() *Cache {
return &Cache{
data: make(map[string]entry),
func New[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]entry[V]),
mu: &sync.RWMutex{},
}
}
// Put stores a value in the cache under the given key.
func (c *Cache) Put(key string, val interface{}) {
func (c *Cache[K, V]) Put(key K, val V) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = entry{nil, val}
c.data[key] = entry[V]{nil, val}
}
// PutTTL stores a value in the cache under the given key. The value will
// be expired after the given ttl.
//
// A 0 ttl value disables the expiration of the value.
func (c *Cache) PutTTL(key string, val interface{}, ttl time.Duration) {
var exp time.Time
if ttl == 0 {
exp = time.Time{}
} else {
exp = time.Now().Add(ttl)
}
func (c *Cache[K, V]) PutTTL(key K, val V, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = entry{&exp, val}
if ttl == 0 {
c.Put(key, val)
return
}
exp := time.Now().Add(ttl)
c.data[key] = entry[V]{&exp, val}
}
// Get returns the value asspciated with the given key.
// The second return values indicates if the cache hs been hit or not.
func (c *Cache) Get(key string) (interface{}, bool) {
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
v, ok := c.data[key]
entry, ok := c.data[key]
c.mu.RUnlock()
if !ok {
return nil, false
var t V
return t, false
}
if v.expirationDate != nil && v.expirationDate.Before(time.Now()) {
if entry.isExpired() {
c.Del(key)
return nil, false
var t V
return t, false
}
return v.value, ok
return entry.value, ok
}
// Del deletes the entry for the givzn key.
// Del deletes the entry for the given key.
// It does not fail if the key does not exist.
func (c *Cache) Del(key string) {
func (c *Cache[K, V]) Del(key K) {
c.mu.Lock()
defer c.mu.Unlock()
@ -85,7 +95,7 @@ func (c *Cache) Del(key string) {
}
// Count returns the total number of entries in the cache (vamid and expired).
func (c *Cache) Count() int {
func (c *Cache[K, V]) Count() int {
c.mu.RLock()
defer c.mu.RUnlock()

View file

@ -1,156 +1,156 @@
package cache
package cache_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"code.bcarlin.net/go/cache"
)
func TestCache(t *testing.T) {
key := "test"
t.Parallel()
c := New()
const key = "test"
t.Log("simple put and get")
t.Run("simple put and get", func(t *testing.T) {
t.Parallel()
value := "foo"
c := cache.New[string, string]()
c.Put(key, value)
value := "foo"
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c.Put(key, value)
val, ok := c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
val2, ok := val.(string)
if !ok {
t.Fatalf("Expected '%v' to have type string, but it has type %T", val, val)
}
got, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
if val2 != value {
t.Fatalf("Expected '%v' to equal 'foo', but it does not", val2)
}
assert.Equal(t, value, got)
})
t.Log("Using the same key overwrites the value")
t.Run("Using the same key overwrites the value", func(t *testing.T) {
t.Parallel()
value = "bar"
c := cache.New[string, string]()
c.Put(key, value)
c.Put(key, "foo")
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
value := "bar"
c.Put(key, value)
val, ok = c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
val2, ok = val.(string)
if !ok {
t.Fatalf("Expected '%v' to have type string, but it has type %T", val, val)
}
got, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
if val2 != value {
t.Fatalf("Expected '%v' to equal 'foo', but it does not", val2)
}
assert.Equal(t, value, got)
})
t.Log("It should tell if the cache is missed")
t.Run("cache is missed", func(t *testing.T) {
t.Parallel()
val, ok = c.Get("does-not-exist")
if ok {
t.Fatalf("Expected cache to not have an entry for key %q, but it did", key)
}
c := cache.New[string, string]()
if val != nil {
t.Fatalf("Expected '%v' to be nil, but it was not", val2)
}
val, ok := c.Get("does-not-exist")
assert.False(t, ok, "Expected cache to not have an entry for key 'does-not-exist', but it did")
assert.Zero(t, val)
})
}
func TestCacheTTL(t *testing.T) {
key := "test"
value := "foo"
t.Parallel()
c := New()
const (
key = "test"
value = "foo"
)
t.Log("Simple put and get a non-expired entry")
t.Run("simple put and get a non-expired entry", func(t *testing.T) {
t.Parallel()
c.PutTTL(key, value, 1*time.Hour)
c := cache.New[string, string]()
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c.PutTTL(key, value, 1*time.Hour)
_, ok := c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
t.Log("Simple put and get a 0 TTL")
_, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
})
c.PutTTL(key, value, 0)
t.Run("simple put and get a 0 TTL", func(t *testing.T) {
t.Parallel()
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c := cache.New[string, string]()
_, ok = c.Get(key)
if !ok {
t.Fatalf("Expected cache to have an entry for key %q, but it did not", key)
}
c.PutTTL(key, value, 0)
t.Log("Simple put and get an expired entry")
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
c.PutTTL(key, value, -1*time.Hour)
_, ok := c.Get(key)
assert.True(t, ok, "Expected cache to have an entry for key %q, but it did not", key)
})
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
t.Run("simple put and get an expired entry", func(t *testing.T) {
t.Parallel()
_, ok = c.Get(key)
if ok {
t.Fatalf("Expected cache to not have an entry for key %q, but it did", key)
}
c := cache.New[string, string]()
if n := c.Count(); n != 0 {
t.Fatalf("Expected %d entries in the cache, got %d", 0, n)
}
c.PutTTL(key, value, -1*time.Hour)
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
_, ok := c.Get(key)
assert.False(t, ok, "Expected cache to not have an entry for key %q, but it did", key)
n = c.Count()
assert.Equal(t, 0, n, "Expected %d entries in the cache, got %d", 0, n)
})
}
func TestCacheDel(t *testing.T) {
key := "test"
value := "foo"
t.Parallel()
c := New()
const (
key = "test"
value = "foo"
)
c.Put(key, value)
t.Run("delete an existing key", func(t *testing.T) {
t.Parallel()
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c := cache.New[string, string]()
t.Log("Delete an existing key from the cache")
c.Put(key, value)
c.Del(key)
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
if n := c.Count(); n != 0 {
t.Fatalf("Expected %d entries in the cache, got %d", 0, n)
}
c.Del(key)
c.Put(key, value)
n = c.Count()
assert.Equal(t, 0, n, "Expected %d entries in the cache, got %d", 0, n)
})
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
t.Run("delete a non-existing key", func(t *testing.T) {
t.Parallel()
t.Log("Delete an existing key from the cache")
c := cache.New[string, string]()
c.Del("does-not-exist")
c.Put(key, value)
if n := c.Count(); n != 1 {
t.Fatalf("Expected %d entries in the cache, got %d", 1, n)
}
c.Del("does-not-exist")
n := c.Count()
assert.Equal(t, 1, n, "Expected %d entries in the cache, got %d", 1, n)
})
}

14
go.mod
View file

@ -1,3 +1,13 @@
module code.bcarlin.xyz/go/cache
module code.bcarlin.net/go/cache
go 1.17
go 1.22.0
toolchain go1.23.4
require github.com/stretchr/testify v1.10.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum Normal file
View file

@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=