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. // It supports exîration dates and can store arbitrary values of any type.
// Keys must be strings. // Keys must be strings.
//
// Cache values are safe to share between goroutines.
package cache package cache
import ( import (
@ -9,75 +11,83 @@ import (
"time" "time"
) )
type entry struct { type entry[V any] struct {
expirationDate *time.Time 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. // Cache is an in-memory key-value store.
type Cache struct { type Cache[K comparable, V any] struct {
data map[string]entry data map[K]entry[V]
mu *sync.RWMutex mu *sync.RWMutex
} }
// New instantiate a new cache. // New instantiate a new cache.
func New() *Cache { func New[K comparable, V any]() *Cache[K, V] {
return &Cache{ return &Cache[K, V]{
data: make(map[string]entry), data: make(map[K]entry[V]),
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
} }
} }
// Put stores a value in the cache under the given key. // 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() c.mu.Lock()
defer c.mu.Unlock() 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 // PutTTL stores a value in the cache under the given key. The value will
// be expired after the given ttl. // be expired after the given ttl.
// //
// A 0 ttl value disables the expiration of the value. // A 0 ttl value disables the expiration of the value.
func (c *Cache) PutTTL(key string, val interface{}, ttl time.Duration) { func (c *Cache[K, V]) PutTTL(key K, val V, ttl time.Duration) {
var exp time.Time
if ttl == 0 {
exp = time.Time{}
} else {
exp = time.Now().Add(ttl)
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() 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. // Get returns the value asspciated with the given key.
// The second return values indicates if the cache hs been hit or not. // 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() c.mu.RLock()
v, ok := c.data[key] entry, ok := c.data[key]
c.mu.RUnlock() c.mu.RUnlock()
if !ok { 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) 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. // 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() c.mu.Lock()
defer c.mu.Unlock() 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). // 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() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()

View file

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