Skip to content

Commit 25c842d

Browse files
committed
initial commit
0 parents  commit 25c842d

File tree

7 files changed

+421
-0
lines changed

7 files changed

+421
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 codehex
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# go-generics-cache
2+
3+
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.
4+
5+
- implemented with [Go Generics](https://go.dev/blog/generics-proposal)
6+
- a thread-safe `map[string]interface{}` with expiration times
7+
8+
## Requirements
9+
10+
Go 1.18 or later.
11+
12+
If Go 1.18 has not been released but you want to try this package, you can easily do so by using the [`gotip`](https://pkg.go.dev/golang.org/dl/gotip) command.
13+
14+
```sh
15+
$ go install golang.org/dl/gotip@latest
16+
$ gotip download # latest commit
17+
$ gotip version
18+
go version devel go1.18-c2397905e0 Sat Nov 13 03:33:55 2021 +0000 darwin/arm64
19+
```
20+
21+
## Install
22+
23+
go get github.com/Code-Hex/go-generics-cache
24+
25+
## Usage
26+
27+
See also [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/example_test.go)
28+
29+
```go
30+
package main
31+
32+
import (
33+
"fmt"
34+
35+
cache "github.com/Code-Hex/go-generics-cache"
36+
)
37+
38+
func main() {
39+
// Create a cache. key as string, value as int.
40+
c1 := cache.New[string, int]()
41+
42+
// Sets the value of int. you can set with expiration option.
43+
c1.Set("foo", 1, cache.WithExpiration(time.Hour))
44+
45+
// the value never expires.
46+
c1.Set("bar", 2)
47+
48+
foo, ok := c.Get("foo")
49+
if ok {
50+
fmt.Println(foo) // 1
51+
}
52+
53+
fmt.Println(c.Keys()) // outputs "foo" "bar" may random
54+
55+
// Create a cache. key as int, value as string.
56+
c2 := cache.New[int, string]()
57+
c2.Set(1, "baz")
58+
baz, ok := c.Get(1)
59+
if ok {
60+
fmt.Println(baz) // "baz"
61+
}
62+
63+
// Create a cache for Number constraint.. key as string, value as int.
64+
nc := cache.NewNumber[string, int]()
65+
nc.Set("age", 26)
66+
67+
// This will be compile error, because string is not satisfied cache.Number constraint.
68+
// nc := cache.NewNumber[string, string]()
69+
70+
incremented, _ := nc.Increment("age", 1)
71+
fmt.Println(incremented) // 27
72+
73+
decremented, _ := nc.Decrement("age", 1)
74+
fmt.Println(decremented) // 26
75+
}
76+
```

cache.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package cache
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"sync"
7+
"time"
8+
)
9+
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
120+
}
121+
122+
// NumberCache is a in-memory cache which is able to store only Number constraint.
123+
type NumberCache[K comparable, V Number] struct {
124+
*Cache[K, V]
125+
}
126+
127+
// NewNumber creates a new cache for Number constraint.
128+
func NewNumber[K comparable, V Number]() *NumberCache[K, V] {
129+
return &NumberCache[K, V]{
130+
Cache: New[K, V](),
131+
}
132+
}
133+
134+
// Increment an item of type Number constraint by n.
135+
// Returns an error if the item was not found or expired. If there is no error, the
136+
// incremented value is returned.
137+
func (nc *NumberCache[K, V]) Increment(k K, n V) (val V, err error) {
138+
got, err := nc.Cache.GetItem(k)
139+
if err != nil {
140+
return val, err
141+
}
142+
143+
nv := got.Value + n
144+
got.Value = nv
145+
nc.Cache.SetItem(k, got)
146+
147+
return nv, nil
148+
}
149+
150+
// Decrement an item of type Number constraint by n.
151+
// Returns an error if the item was not found or expired. If there is no error, the
152+
// decremented value is returned.
153+
func (nc *NumberCache[K, V]) Decrement(k K, n V) (val V, err error) {
154+
got, err := nc.Cache.GetItem(k)
155+
if err != nil {
156+
return val, err
157+
}
158+
159+
nv := got.Value - n
160+
got.Value = nv
161+
nc.Cache.SetItem(k, got)
162+
163+
return nv, nil
164+
}

cache_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cache
2+
3+
import (
4+
"errors"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestHasExpired(t *testing.T) {
10+
cases := []struct {
11+
name string
12+
exp time.Duration
13+
createdAt time.Time
14+
current time.Time
15+
want bool
16+
}{
17+
// expiration == createdAt + exp
18+
{
19+
name: "item expiration is zero",
20+
want: false,
21+
},
22+
{
23+
name: "item expiration > current time",
24+
exp: time.Hour * 24,
25+
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
26+
current: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
27+
want: false,
28+
},
29+
{
30+
name: "item expiration < current time",
31+
exp: time.Hour * 24,
32+
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
33+
current: time.Date(2009, time.November, 12, 23, 0, 0, 0, time.UTC),
34+
want: true,
35+
},
36+
{
37+
name: "item expiration == current time",
38+
exp: time.Second,
39+
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
40+
current: time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC),
41+
want: false,
42+
},
43+
}
44+
for _, tc := range cases {
45+
t.Run(tc.name, func(t *testing.T) {
46+
backup := nowFunc
47+
nowFunc = func() time.Time { return tc.current }
48+
defer func() { nowFunc = backup }()
49+
50+
it := Item[int]{
51+
Expiration: tc.exp,
52+
CreatedAt: tc.createdAt,
53+
}
54+
if got := it.HasExpired(); tc.want != got {
55+
t.Fatalf("want %v, but got %v", tc.want, got)
56+
}
57+
})
58+
}
59+
}
60+
61+
func TestGetItemExpired(t *testing.T) {
62+
c := New[struct{}, int]()
63+
c.SetItem(struct{}{}, Item[int]{
64+
Value: 1,
65+
Expiration: time.Hour * 24,
66+
CreatedAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
67+
})
68+
69+
backup := nowFunc
70+
nowFunc = func() time.Time {
71+
return time.Date(2009, time.November, 12, 23, 0, 0, 0, time.UTC)
72+
}
73+
defer func() { nowFunc = backup }()
74+
75+
v, err := c.GetItem(struct{}{})
76+
if !errors.Is(err, ErrExpired) {
77+
t.Errorf("want error %v but got %v", ErrExpired, err)
78+
}
79+
zeroValItem := Item[int]{}
80+
if zeroValItem != v {
81+
t.Errorf("want %v but got %v", zeroValItem, v)
82+
}
83+
84+
}

constraint.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package cache
2+
3+
import "constraints"
4+
5+
// Number is a constraint that permits any numeric types.
6+
type Number interface {
7+
constraints.Integer | constraints.Float | constraints.Complex
8+
}

0 commit comments

Comments
 (0)