feat: use generics
This commit is contained in:
parent
cb38484486
commit
d61b3d6865
5 changed files with 2172 additions and 669 deletions
2553
.golangci.yml
2553
.golangci.yml
File diff suppressed because it is too large
Load diff
66
cache.go
66
cache.go
|
@ -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()
|
||||||
|
|
||||||
|
|
198
cache_test.go
198
cache_test.go
|
@ -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
14
go.mod
|
@ -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
10
go.sum
Normal 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=
|
Loading…
Reference in a new issue