-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
190 lines (161 loc) · 4.02 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Package provides simple, fast, thread-safe in-memory cache with by-key TTL expiration.
// Supporting generic value types.
package mcache
import (
"errors"
"sync"
"time"
)
// Errors for cache
var (
ErrKeyNotFound = errors.New("key not found")
ErrExpired = errors.New("key expired")
)
// CacheItem is a struct for cache item.
type CacheItem[T any] struct {
value T
expiration time.Time
}
// Cache is a struct for cache.
type Cache[T any] struct {
initialSize int
data map[string]*CacheItem[T]
sync.RWMutex
}
// Cacher is an interface for cache.
type Cacher[T any] interface {
Set(key string, value T, ttl time.Duration) bool
Get(key string) (T, error)
Has(key string) (bool, error)
Del(key string) error
Cleanup()
Clear() error
}
// NewCache is a constructor for Cache.
func NewCache[T any](options ...func(*Cache[T])) *Cache[T] {
c := &Cache[T]{
data: make(map[string]*CacheItem[T]),
}
for _, option := range options {
option(c)
}
return c
}
// common method for checking if item is expired
func (cacheItem CacheItem[T]) expired() bool {
if !cacheItem.expiration.IsZero() && cacheItem.expiration.Before(time.Now()) {
return true
}
return false
}
// Set is a method for setting key-value pair.
// If key already exists, and it's not expired, return false.
// If key already exists, but it's expired, set new value and return true.
// If key doesn't exist, set new value and return true.
// If ttl is 0, set value without expiration.
func (c *Cache[T]) Set(key string, value T, ttl time.Duration) bool {
c.Lock()
defer c.Unlock()
cached, ok := c.data[key]
if ok {
if !cached.expired() {
return false
}
}
var expiration time.Time
if ttl > time.Duration(0) {
expiration = time.Now().Add(ttl)
}
c.data[key] = &CacheItem[T]{
value: value,
expiration: expiration,
}
return true
}
// Get is a method for getting value by key.
// If key doesn't exist, return error.
// If key exists, but it's expired, delete key, return zero value and error.
// If key exists and it's not expired, return value.
func (c *Cache[T]) Get(key string) (T, error) {
var none T
c.Lock()
defer c.Unlock()
item, ok := c.data[key]
if !ok {
return none, ErrKeyNotFound
}
if item.expired() {
delete(c.data, key)
return none, ErrExpired
}
return c.data[key].value, nil
}
// Has checks if key exists and if it's expired.
// If key doesn't exist, return false.
// If key exists, but it's expired, return false and delete key.
// If key exists and it's not expired, return true.
func (c *Cache[T]) Has(key string) (bool, error) {
c.Lock()
defer c.Unlock()
item, ok := c.data[key]
if !ok {
return false, ErrKeyNotFound
}
if item.expired() {
delete(c.data, key)
return false, ErrExpired
}
return true, nil
}
// Del deletes a key-value pair.
func (c *Cache[T]) Del(key string) error {
_, err := c.Has(key)
if err != nil {
return err
}
// parallel goroutine can delete key right here
// or even perform Clear() operation
// but it doen't matter
c.Lock()
delete(c.data, key)
c.Unlock()
return nil
}
// Clears cache by replacing it with a clean one.
func (c *Cache[T]) Clear() error {
c.Lock()
c.data = make(map[string]*CacheItem[T], c.initialSize)
c.Unlock()
return nil
}
// Cleanup deletes expired keys from cache by copying non-expired keys to a new map.
func (c *Cache[T]) Cleanup() {
c.Lock()
defer c.Unlock()
data := make(map[string]*CacheItem[T], c.initialSize)
for k, v := range c.data {
if !v.expired() {
data[k] = v
}
}
c.data = data
}
// WithCleanup is a functional option for setting interval to run Cleanup goroutine.
func WithCleanup[T any](ttl time.Duration) func(*Cache[T]) {
return func(c *Cache[T]) {
go func() {
for {
c.Cleanup()
time.Sleep(ttl)
}
}()
}
}
// WithSize is a functional option for setting cache initial size. So it won't grow dynamically,
// go will allocate appropriate number of buckets.
func WithSize[T any](size int) func(*Cache[T]) {
return func(c *Cache[T]) {
c.data = make(map[string]*CacheItem[T], size)
c.initialSize = size
}
}