Skip to content

Commit 8404049

Browse files
authored
Merge pull request #4 from Code-Hex/add/simple
Add simple
2 parents 8ade14b + 1ccfd61 commit 8404049

File tree

8 files changed

+306
-310
lines changed

8 files changed

+306
-310
lines changed

README.md

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
go-generics-cache is an in-memory key:value store/cache that is suitable for applications running on a single machine. This in-memory cache uses [Go Generics](https://go.dev/blog/generics-proposal) which will be introduced in 1.18.
66

7+
- a thread-safe
78
- implemented with [Go Generics](https://go.dev/blog/generics-proposal)
8-
- a thread-safe `map[string]interface{}` with expiration times
9+
- Simple `map[string]interface{}` with expiration times
10+
- See [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/simple/example_test.go)
911
- LRU cache
1012
- See [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/lru/example_test.go)
1113

@@ -30,8 +32,6 @@ go version devel go1.18-c2397905e0 Sat Nov 13 03:33:55 2021 +0000 darwin/arm64
3032

3133
See also [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/example_test.go)
3234

33-
playground: https://gotipplay.golang.org/p/FXRk6ngYV-s
34-
3535
```go
3636
package main
3737

@@ -40,44 +40,24 @@ import (
4040
"time"
4141

4242
cache "github.com/Code-Hex/go-generics-cache"
43+
"github.com/Code-Hex/go-generics-cache/simple"
4344
)
4445

4546
func main() {
46-
// Create a cache. key as string, value as int.
47-
c1 := cache.New[string, int]()
48-
49-
// Sets the value of int. you can set with expiration option.
50-
c1.Set("foo", 1, cache.WithExpiration(time.Hour))
51-
52-
// the value never expires.
53-
c1.Set("bar", 2)
54-
55-
foo, ok := c1.Get("foo")
56-
if ok {
57-
fmt.Println(foo) // 1
58-
}
59-
60-
fmt.Println(c1.Keys()) // outputs "foo" "bar" may random
61-
62-
// Create a cache. key as int, value as string.
63-
c2 := cache.New[int, string]()
64-
c2.Set(1, "baz")
65-
baz, ok := c2.Get(1)
66-
if ok {
67-
fmt.Println(baz) // "baz"
68-
}
47+
// Create a simple cache. key as string, value as int.
48+
simpleCache := simple.New[string, int](simple.WithExpiration(time.Hour))
6949

70-
// Create a cache for Number constraint.. key as string, value as int.
71-
nc := cache.NewNumber[string, int]()
50+
// Create a cache for Number constraint. key as string, value as int.
51+
nc := cache.NewNumber[string, int](simpleCache)
7252
nc.Set("age", 26)
7353

7454
// This will be compile error, because string is not satisfied cache.Number constraint.
75-
// nc := cache.NewNumber[string, string]()
55+
// nc := cache.NewNumber[string, string](simpleCache)
7656

77-
incremented, _ := nc.Increment("age", 1)
57+
incremented := nc.Increment("age", 1)
7858
fmt.Println(incremented) // 27
7959

80-
decremented, _ := nc.Decrement("age", 1)
60+
decremented := nc.Decrement("age", 1)
8161
fmt.Println(decremented) // 26
8262
}
8363
```

cache.go

Lines changed: 19 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,50 @@
11
package cache
22

33
import (
4-
"errors"
5-
"fmt"
64
"sync"
7-
"time"
85
)
96

10-
var (
11-
// ErrNotFound is an error which indicate an item is not found.
12-
ErrNotFound = errors.New("not found item")
13-
14-
// ErrExpired is an error which indicate an item is expired.
15-
ErrExpired = errors.New("expired item")
16-
)
17-
18-
// Item is an item
19-
type Item[T any] struct {
20-
Value T
21-
Expiration time.Duration
22-
CreatedAt time.Time
23-
}
24-
25-
var nowFunc = time.Now
26-
27-
// HasExpired returns true if the item has expired.
28-
// If the item's expiration is zero value, returns false.
29-
func (i Item[T]) HasExpired() bool {
30-
if i.Expiration <= 0 {
31-
return false
32-
}
33-
return i.CreatedAt.Add(i.Expiration).Before(nowFunc())
34-
}
35-
36-
// Cache is a base struct for creating in-memory cache.
37-
type Cache[K comparable, V any] struct {
38-
items map[K]Item[V]
39-
mu sync.RWMutex
40-
}
41-
42-
// New creates a new cache.
43-
func New[K comparable, V any]() *Cache[K, V] {
44-
return &Cache[K, V]{
45-
items: make(map[K]Item[V]),
46-
}
47-
}
48-
49-
// ItemOption is an option for cache item.
50-
type ItemOption func(o *options)
51-
52-
type options struct {
53-
expiration time.Duration // default none
54-
}
55-
56-
// WithExpiration is an option to set expiration time for any items.
57-
func WithExpiration(exp time.Duration) ItemOption {
58-
return func(o *options) {
59-
o.expiration = exp
60-
}
61-
}
62-
63-
// Set sets any item to the cache. replacing any existing item.
64-
// The default item never expires.
65-
func (c *Cache[K, V]) Set(k K, v V, opts ...ItemOption) {
66-
o := new(options)
67-
for _, optFunc := range opts {
68-
optFunc(o)
69-
}
70-
item := Item[V]{
71-
Value: v,
72-
Expiration: o.expiration,
73-
CreatedAt: nowFunc(),
74-
}
75-
c.SetItem(k, item)
76-
}
77-
78-
// SetItem sets any item to the cache. replacing any existing item.
79-
// The default item never expires.
80-
func (c *Cache[K, V]) SetItem(k K, v Item[V]) {
81-
c.mu.Lock()
82-
c.items[k] = v
83-
c.mu.Unlock()
84-
}
85-
86-
// Get gets an item from the cache.
87-
// Returns the item or zero value, and a bool indicating whether the key was found.
88-
func (c *Cache[K, V]) Get(k K) (val V, ok bool) {
89-
item, err := c.GetItem(k)
90-
if err != nil {
91-
return
92-
}
93-
return item.Value, true
94-
}
95-
96-
// GetItem gets an item from the cache.
97-
// Returns an error if the item was not found or expired. If there is no error, the
98-
// incremented value is returned.
99-
func (c *Cache[K, V]) GetItem(k K) (val Item[V], _ error) {
100-
c.mu.RLock()
101-
defer c.mu.RUnlock()
102-
103-
got, found := c.items[k]
104-
if !found {
105-
return val, fmt.Errorf("key[%v]: %w", k, ErrNotFound)
106-
}
107-
if got.HasExpired() {
108-
return val, fmt.Errorf("key[%v]: %w", k, ErrExpired)
109-
}
110-
return got, nil
111-
}
112-
113-
// Keys returns cache keys. the order is random.
114-
func (c *Cache[K, _]) Keys() []K {
115-
ret := make([]K, 0, len(c.items))
116-
for key := range c.items {
117-
ret = append(ret, key)
118-
}
119-
return ret
7+
// Cache is a common-cache interface.
8+
type Cache[K comparable, V any] interface {
9+
Get(key K) (value V, ok bool)
10+
Set(key K, val V)
12011
}
12112

12213
// NumberCache is a in-memory cache which is able to store only Number constraint.
12314
type NumberCache[K comparable, V Number] struct {
124-
*Cache[K, V]
15+
Cache[K, V]
12516
// nmu is used to do lock in Increment/Decrement process.
12617
// Note that this must be here as a separate mutex because mu in Cache struct is Locked in GetItem,
12718
// and if we call mu.Lock in Increment/Decrement, it will cause deadlock.
12819
nmu sync.Mutex
12920
}
13021

13122
// NewNumber creates a new cache for Number constraint.
132-
func NewNumber[K comparable, V Number]() *NumberCache[K, V] {
23+
func NewNumber[K comparable, V Number](baseCache Cache[K, V]) *NumberCache[K, V] {
13324
return &NumberCache[K, V]{
134-
Cache: New[K, V](),
25+
Cache: baseCache,
13526
}
13627
}
13728

13829
// Increment an item of type Number constraint by n.
139-
// Returns an error if the item was not found or expired. If there is no error, the
140-
// incremented value is returned.
141-
func (nc *NumberCache[K, V]) Increment(k K, n V) (val V, err error) {
30+
// Returns the incremented value.
31+
func (nc *NumberCache[K, V]) Increment(key K, n V) V {
14232
// In order to avoid lost update, we must lock whole Increment/Decrement process.
14333
nc.nmu.Lock()
14434
defer nc.nmu.Unlock()
145-
got, err := nc.Cache.GetItem(k)
146-
if err != nil {
147-
return val, err
148-
}
149-
150-
nv := got.Value + n
151-
got.Value = nv
152-
nc.Cache.SetItem(k, got)
153-
154-
return nv, nil
35+
got, _ := nc.Cache.Get(key)
36+
nv := got + n
37+
nc.Cache.Set(key, nv)
38+
return nv
15539
}
15640

15741
// Decrement an item of type Number constraint by n.
158-
// Returns an error if the item was not found or expired. If there is no error, the
159-
// decremented value is returned.
160-
func (nc *NumberCache[K, V]) Decrement(k K, n V) (val V, err error) {
42+
// Returns the decremented value.
43+
func (nc *NumberCache[K, V]) Decrement(key K, n V) V {
16144
nc.nmu.Lock()
16245
defer nc.nmu.Unlock()
163-
got, err := nc.Cache.GetItem(k)
164-
if err != nil {
165-
return val, err
166-
}
167-
168-
nv := got.Value - n
169-
got.Value = nv
170-
nc.Cache.SetItem(k, got)
171-
172-
return nv, nil
46+
got, _ := nc.Cache.Get(key)
47+
nv := got - n
48+
nc.Cache.Set(key, nv)
49+
return nv
17350
}

cache_test.go

Lines changed: 8 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,23 @@
1-
package cache
1+
package cache_test
22

33
import (
4-
"errors"
54
"sync"
65
"testing"
7-
"time"
8-
)
9-
10-
func TestHasExpired(t *testing.T) {
11-
cases := []struct {
12-
name string
13-
exp time.Duration
14-
createdAt time.Time
15-
current time.Time
16-
want bool
17-
}{
18-
// expiration == createdAt + exp
19-
{
20-
name: "item expiration is zero",
21-
want: false,
22-
},
23-
{
24-
name: "item expiration > current time",
25-
exp: time.Hour * 24,
26-
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
27-
current: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
28-
want: false,
29-
},
30-
{
31-
name: "item expiration < current time",
32-
exp: time.Hour * 24,
33-
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
34-
current: time.Date(2009, time.November, 12, 23, 0, 0, 0, time.UTC),
35-
want: true,
36-
},
37-
{
38-
name: "item expiration == current time",
39-
exp: time.Second,
40-
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
41-
current: time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC),
42-
want: false,
43-
},
44-
}
45-
for _, tc := range cases {
46-
t.Run(tc.name, func(t *testing.T) {
47-
backup := nowFunc
48-
nowFunc = func() time.Time { return tc.current }
49-
defer func() { nowFunc = backup }()
50-
51-
it := Item[int]{
52-
Expiration: tc.exp,
53-
CreatedAt: tc.createdAt,
54-
}
55-
if got := it.HasExpired(); tc.want != got {
56-
t.Fatalf("want %v, but got %v", tc.want, got)
57-
}
58-
})
59-
}
60-
}
61-
62-
func TestGetItemExpired(t *testing.T) {
63-
c := New[struct{}, int]()
64-
c.SetItem(struct{}{}, Item[int]{
65-
Value: 1,
66-
Expiration: time.Hour * 24,
67-
CreatedAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
68-
})
696

70-
backup := nowFunc
71-
nowFunc = func() time.Time {
72-
return time.Date(2009, time.November, 12, 23, 0, 0, 0, time.UTC)
73-
}
74-
defer func() { nowFunc = backup }()
75-
76-
v, err := c.GetItem(struct{}{})
77-
if !errors.Is(err, ErrExpired) {
78-
t.Errorf("want error %v but got %v", ErrExpired, err)
79-
}
80-
zeroValItem := Item[int]{}
81-
if zeroValItem != v {
82-
t.Errorf("want %v but got %v", zeroValItem, v)
83-
}
84-
85-
}
7+
cache "github.com/Code-Hex/go-generics-cache"
8+
"github.com/Code-Hex/go-generics-cache/simple"
9+
)
8610

8711
func TestMultiThreadIncr(t *testing.T) {
88-
nc := NewNumber[string, int]()
12+
nc := cache.NewNumber[string, int](simple.New[string, int]())
8913
nc.Set("counter", 0)
9014

9115
var wg sync.WaitGroup
9216

9317
for i := 0; i < 100; i++ {
9418
wg.Add(1)
9519
go func() {
96-
_, err := nc.Increment("counter", 1)
97-
if err != nil {
98-
t.Logf("err: %v", err)
99-
}
20+
_ = nc.Increment("counter", 1)
10021
wg.Done()
10122
}()
10223
}
@@ -109,18 +30,15 @@ func TestMultiThreadIncr(t *testing.T) {
10930
}
11031

11132
func TestMultiThreadDecr(t *testing.T) {
112-
nc := NewNumber[string, int]()
33+
nc := cache.NewNumber[string, int](simple.New[string, int]())
11334
nc.Set("counter", 100)
11435

11536
var wg sync.WaitGroup
11637

11738
for i := 0; i < 100; i++ {
11839
wg.Add(1)
11940
go func() {
120-
_, err := nc.Decrement("counter", 1)
121-
if err != nil {
122-
t.Logf("err: %v", err)
123-
}
41+
_ = nc.Decrement("counter", 1)
12442
wg.Done()
12543
}()
12644
}

0 commit comments

Comments
 (0)