Skip to content

Commit 4ba4158

Browse files
Merge pull request #395 from AniketS01/main
added inmemory_cache in go/cache
2 parents 5796161 + 5fe3d4d commit 4ba4158

2 files changed

Lines changed: 332 additions & 0 deletions

File tree

go/cache/inmemory_cache.go

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// This is a generic, thread-safe in-memory cache implementation in Go, designed to store key-value pairs with optional expiration times. It supports:
2+
3+
// Generic keys (strings only) and generic values (any)
4+
5+
// Per-item expiration and default expiration
6+
7+
// Automatic cleanup of expired items via a background goroutine
8+
9+
// Safe concurrent access using sync.RWMutex
10+
11+
// Common cache operations: Set, Get, Update, Delete, Flush, Count, List, MapToCache
12+
13+
// Designed to be efficient: O(1) access time for most operations
14+
15+
// It is suitable for caching data in-memory within a single Go application and provides a lightweight alternative to external caching systems like Redis for local use cases.
16+
17+
package inmemory_cache
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"runtime"
23+
"sync"
24+
"time"
25+
)
26+
27+
const (
28+
NoExpiration time.Duration = -1 // Constant to indicate no expiration
29+
Defaultexpires time.Duration = 0 // Constant to use default expiration time
30+
)
31+
32+
// Item represents a cache entry holding a value and its expiration timestamp
33+
type Item[V any] struct {
34+
value V // Stored value
35+
expires int64 // Expiration time in UnixNano (0 or negative means no expiration)
36+
}
37+
38+
// Internal cache struct (not exposed directly)
39+
type cache[K ~string, V any] struct {
40+
mu sync.RWMutex // Mutex for thread-safe access
41+
items map[K]*Item[V] // Actual map storing cache items
42+
done chan struct{} // Channel to stop cleanup goroutine
43+
expTime time.Duration // Default expiration time for items
44+
cleanupInt time.Duration // Interval between cleanup runs
45+
}
46+
47+
// Exposed Cache type embedding the internal cache
48+
type Cache[K ~string, V any] struct {
49+
*cache[K, V]
50+
}
51+
52+
// Time Complexity: O(1)
53+
// Space Complexity: O(1)
54+
// Creates a new internal cache instance
55+
func newCache[K ~string, V any](expTime, cleanupInt time.Duration, item map[K]*Item[V]) *cache[K, V] {
56+
c := &cache[K, V]{
57+
mu: sync.RWMutex{},
58+
items: item,
59+
expTime: expTime,
60+
cleanupInt: cleanupInt,
61+
done: make(chan struct{}),
62+
}
63+
return c
64+
}
65+
66+
// Time Complexity: O(1)
67+
// Space Complexity: O(1)
68+
// Public API: creates and returns a new cache instance with optional background cleanup
69+
func New[K ~string, V any](expTime, cleanupTime time.Duration) *Cache[K, V] {
70+
items := make(map[K]*Item[V])
71+
c := newCache(expTime, cleanupTime, items)
72+
73+
if cleanupTime > 0 {
74+
// Start background cleanup
75+
go c.cleanup()
76+
// Set finalizer to stop cleanup when cache is garbage collected
77+
runtime.SetFinalizer(c, stopCleanup[K, V])
78+
}
79+
80+
return &Cache[K, V]{c}
81+
}
82+
83+
// Time Complexity: O(1)
84+
// Space Complexity: O(1)
85+
// Adds a new item to the cache, returns error if key already exists
86+
func (c *Cache[K, V]) Set(key K, val V, d time.Duration) error {
87+
item, err := c.Get(key)
88+
if item != nil && err == nil {
89+
return fmt.Errorf("item with key '%v' already exists. Use the Update method", key)
90+
}
91+
c.add(key, val, d)
92+
return nil
93+
}
94+
95+
// Time Complexity: O(1)
96+
// Space Complexity: O(1)
97+
// Adds a value using default expiration
98+
func (c *Cache[K, V]) SetDefault(key K, val V) error {
99+
return c.Set(key, val, Defaultexpires)
100+
}
101+
102+
// Time Complexity: O(1)
103+
// Space Complexity: O(1)
104+
// Internal method to add or update a cache item
105+
func (c *Cache[K, V]) add(key K, val V, d time.Duration) error {
106+
var exp int64
107+
108+
// Handle expiration logic
109+
if d == Defaultexpires {
110+
d = c.expTime
111+
}
112+
if d > 0 {
113+
exp = time.Now().Add(d).UnixNano()
114+
} else if d < 0 {
115+
exp = int64(NoExpiration)
116+
}
117+
118+
// Prevent overwriting existing items
119+
item, err := c.Get(key)
120+
if item != nil && err != nil {
121+
return fmt.Errorf("item with key '%v' already exists", key)
122+
}
123+
124+
// Optional value validation
125+
switch any(val).(type) {
126+
case string:
127+
if len(any(val).(string)) == 0 {
128+
return fmt.Errorf("value of type string cannot be empty")
129+
}
130+
}
131+
132+
// Add to map with write lock
133+
c.mu.Lock()
134+
c.items[key] = &Item[V]{value: val, expires: exp}
135+
c.mu.Unlock()
136+
137+
return nil
138+
}
139+
140+
// Time Complexity: O(1)
141+
// Space Complexity: O(1)
142+
// Retrieves a cache item if it exists and is not expired
143+
func (c *Cache[K, V]) Get(key K) (*Item[V], error) {
144+
c.mu.RLock()
145+
if item, ok := c.items[key]; ok {
146+
if item.expires > 0 {
147+
now := time.Now().UnixNano()
148+
if now > item.expires {
149+
c.mu.RUnlock()
150+
return nil, fmt.Errorf("item with key '%v' expired", key)
151+
}
152+
}
153+
c.mu.RUnlock()
154+
return item, nil
155+
}
156+
c.mu.RUnlock()
157+
return nil, fmt.Errorf("item with key '%v' not found", key)
158+
}
159+
160+
// Safely retrieves the value from an Item
161+
func (it *Item[V]) Val() V {
162+
var v V
163+
if it != nil {
164+
return it.value
165+
}
166+
return v
167+
}
168+
169+
// Time Complexity: O(1)
170+
// Space Complexity: O(1)
171+
// Updates an existing cache item
172+
func (c *Cache[K, V]) Update(key K, val V, d time.Duration) error {
173+
item, err := c.Get(key)
174+
if item != nil && err != nil {
175+
return err
176+
}
177+
return c.add(key, val, d)
178+
} // Time Complexity: O(1)
179+
// Space Complexity: O(1)
180+
181+
// Time Complexity: O(1)
182+
// Space Complexity: O(1)
183+
// Deletes a cache item
184+
func (c *Cache[K, V]) Delete(key K) error {
185+
c.mu.Lock()
186+
defer c.mu.Unlock()
187+
return c.delete(key)
188+
}
189+
190+
// Time Complexity: O(1)
191+
// Space Complexity: O(1)
192+
// Internal delete logic
193+
func (c *cache[K, V]) delete(key K) error {
194+
if _, ok := c.items[key]; ok {
195+
delete(c.items, key)
196+
return nil
197+
}
198+
return fmt.Errorf("item with key '%v' does not exists", key)
199+
}
200+
201+
// Deletes all expired items from the cache
202+
func (c *cache[K, V]) DeleteExpired() error {
203+
var err error
204+
now := time.Now().UnixNano()
205+
206+
c.mu.Lock()
207+
for k, item := range c.items {
208+
if now > item.expires && item.expires != int64(NoExpiration) {
209+
if e := c.delete(k); e != nil {
210+
err = errors.Join(err, e)
211+
}
212+
}
213+
}
214+
c.mu.Unlock()
215+
216+
return errors.Unwrap(err)
217+
}
218+
219+
// Clears all items from the cache
220+
func (c *Cache[K, V]) Flush() {
221+
c.mu.Lock()
222+
c.items = make(map[K]*Item[V])
223+
c.mu.Unlock()
224+
}
225+
226+
// Returns the underlying items map (shallow copy not made)
227+
func (c *Cache[K, V]) List() map[K]*Item[V] {
228+
c.mu.RLock()
229+
defer c.mu.RUnlock()
230+
return c.items
231+
}
232+
233+
// Time Complexity: O(1)
234+
// Space Complexity: O(1)
235+
// Returns the count of items in the cache
236+
func (c *Cache[K, V]) Count() int {
237+
c.mu.RLock()
238+
n := len(c.items)
239+
c.mu.RUnlock()
240+
return n
241+
}
242+
243+
// Bulk inserts items into the cache
244+
func (c *Cache[K, V]) MapToCache(m map[K]V, d time.Duration) error {
245+
var err error
246+
for k, v := range m {
247+
e := c.Set(k, v, d)
248+
err = errors.Join(err, e)
249+
}
250+
return errors.Unwrap(err)
251+
}
252+
253+
// Checks if a specific key is expired
254+
func (c *Cache[K, V]) IsExpired(key K) bool {
255+
item, err := c.Get(key)
256+
if item != nil && err != nil {
257+
if item.expires > time.Now().UnixNano() {
258+
return true
259+
}
260+
}
261+
return false
262+
}
263+
264+
// Background cleanup process to remove expired items periodically
265+
func (c *cache[K, V]) cleanup() {
266+
tick := time.NewTicker(c.cleanupInt)
267+
for {
268+
select {
269+
case <-tick.C:
270+
c.DeleteExpired()
271+
case <-c.done:
272+
tick.Stop()
273+
return
274+
}
275+
}
276+
}
277+
278+
// Time Complexity: O(1)
279+
// Space Complexity: O(1)
280+
// Finalizer to stop cleanup goroutine when cache is GC'd
281+
func stopCleanup[K ~string, V any](c *cache[K, V]) {
282+
c.done <- struct{}{}
283+
}

go/cache/inmemory_cache_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//this is to test inmemory_cache.go
2+
3+
package inmemory_cache
4+
5+
import (
6+
"testing"
7+
"time"
8+
)
9+
10+
type TestStruct struct {
11+
Num int
12+
Children []*TestStruct
13+
}
14+
15+
func TestCache(t *testing.T) {
16+
tc := New[string, int](NoExpiration, 0)
17+
18+
a, found := tc.Get("a")
19+
if found == nil {
20+
t.Error("Getting A found value that shouldn't exist:", a)
21+
}
22+
23+
b, found := tc.Get("b")
24+
if found == nil {
25+
t.Error("Getting B found value that shouldn't exist:", b)
26+
}
27+
28+
c, found := tc.Get("c")
29+
if found == nil {
30+
t.Error("Getting C found value that shouldn't exist:", c)
31+
}
32+
33+
tc.Set("a", 1, NoExpiration)
34+
35+
x, found := tc.Get("a")
36+
if found != nil {
37+
t.Error("a was not found while getting a2:", x)
38+
}
39+
40+
}
41+
42+
func TestCacheTimes(t *testing.T) {
43+
44+
tc := New[string, int](50*time.Millisecond, 1*time.Millisecond)
45+
tc.Set("a", 1, Defaultexpires)
46+
tc.Set("b", 2, NoExpiration)
47+
tc.Set("c", 3, 20*time.Millisecond)
48+
tc.Set("d", 4, 70*time.Millisecond)
49+
}

0 commit comments

Comments
 (0)