Skip to content

Commit 4a6e12a

Browse files
author
John Farley
committed
Origin check in
1 parent 5908006 commit 4a6e12a

3 files changed

Lines changed: 630 additions & 0 deletions

File tree

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# go-simple-cache
2+
3+
go-simple-cache is an in-memory key:value store/cache similar to memcached that is suitable for applications running on a single machine. Its major advantage is that, being essentially a thread-safe map[string]interface{} with expiration times, it doesn't need to serialize or transmit its contents over the network.
4+
5+
Any object can be stored, for a given duration or forever, and the cache can be safely used by multiple goroutines.
6+
7+
Source code originally forked from:
8+
https://github.com/patrickmn/go-cache
9+
10+
This forked version of go-cache is a simplified version of the original. This version
11+
has the following changes from the original:
12+
13+
1. expired items stay in the cache, but a user provided callback function is executed when the cache expires
14+
2. cache expiration is applied to the entire cache, rather than to individual keys
15+
3. removed increment/decrement feature
16+
4. removed ability to persist cache to disk
17+
5. removed auto eviction of expired items
18+
19+
### Use Case
20+
21+
This version of the library is ideal as a simple lookup cache that is populated by a relatively static list
22+
of items that have a limited life or that change infrequently. For example, you fetch a list of records from
23+
a lookup table in a database and put them in the cache with a timeout of 24hrs. The data in the database changes
24+
infrequently, which justifies the 24hr timeout, and performing the lookup from the cache is faster than going
25+
back to the database. When the 24hr timeout is reached, a callback function is executed and the cache is refreshed
26+
with the latest data from the database.
27+
28+
### Installation
29+
30+
`go get github.com/jfarleyx/go-simple-cache`
31+
32+
### Usage
33+
34+
```go
35+
import (
36+
"fmt"
37+
"github.com/jfarleyx/go-simple-cache"
38+
"time"
39+
)
40+
41+
func main() {
42+
// Create a cache with a default expiration time of 5 hours
43+
c := cache.New(5*time.Hour)
44+
45+
// Provide a callback function that is called when the cache expires
46+
myfunc := func() {
47+
// e.g. fetch current data and refill cache
48+
}
49+
c.cache.OnExpired(myfunc)
50+
51+
// Set the value of the key "foo" to "bar"
52+
c.Set("foo", "bar")
53+
54+
// Get the string associated with the key "foo" from the cache
55+
foo, found := c.Get("foo")
56+
if found {
57+
fmt.Println(foo)
58+
}
59+
60+
// Since Go is statically typed, and cache values can be anything, type
61+
// assertion is needed when values are being passed to functions that don't
62+
// take arbitrary types, (i.e. interface{}). The simplest way to do this for
63+
// values which will only be used once--e.g. for passing to another
64+
// function--is:
65+
foo, found := c.Get("foo")
66+
if found {
67+
MyFunction(foo.(string))
68+
}
69+
70+
// This gets tedious if the value is used several times in the same function.
71+
// You might do either of the following instead:
72+
if x, found := c.Get("foo"); found {
73+
foo := x.(string)
74+
// ...
75+
}
76+
// or
77+
var foo string
78+
if x, found := c.Get("foo"); found {
79+
foo = x.(string)
80+
}
81+
// ...
82+
// foo can then be passed around freely as a string
83+
84+
// Want performance? Store pointers!
85+
c.Set("foo", &MyStruct)
86+
if x, found := c.Get("foo"); found {
87+
foo := x.(*MyStruct)
88+
// ...
89+
}
90+
}
91+
```

cache.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package cache
2+
3+
import (
4+
"fmt"
5+
"runtime"
6+
"sync"
7+
"time"
8+
)
9+
10+
// Item represents an item in the cache.
11+
type Item struct {
12+
Object interface{}
13+
Expiration int64
14+
}
15+
16+
// Expired returns true if the item has expired.
17+
func (item Item) Expired() bool {
18+
if item.Expiration == 0 {
19+
return false
20+
}
21+
return time.Now().UnixNano() > item.Expiration
22+
}
23+
24+
// Cache is the cache object
25+
type Cache struct {
26+
*cache
27+
// If this is confusing, see the comment at the bottom of New()
28+
}
29+
30+
type cache struct {
31+
expiration time.Duration
32+
items map[string]Item
33+
mu sync.RWMutex
34+
onExpired func()
35+
janitor *janitor
36+
}
37+
38+
// Set add an item to the cache, replacing any existing item.
39+
func (c *cache) Set(k string, x interface{}) {
40+
// "Inlining" of set
41+
var e = time.Now().Add(c.expiration).UnixNano()
42+
43+
c.mu.Lock()
44+
c.items[k] = Item{
45+
Object: x,
46+
Expiration: e,
47+
}
48+
// TODO: Calls to mu.Unlock are currently not deferred because defer
49+
// adds ~200 ns (as of go1.)
50+
c.mu.Unlock()
51+
}
52+
53+
func (c *cache) set(k string, x interface{}) {
54+
var e = time.Now().Add(c.expiration).UnixNano()
55+
c.items[k] = Item{
56+
Object: x,
57+
Expiration: e,
58+
}
59+
}
60+
61+
// Replace set a new value for the cache key only if it already exists. Returns an error otherwise.
62+
func (c *cache) Replace(k string, x interface{}) error {
63+
c.mu.Lock()
64+
_, found := c.get(k)
65+
if !found {
66+
c.mu.Unlock()
67+
return fmt.Errorf("Item %s doesn't exist", k)
68+
}
69+
c.set(k, x)
70+
c.mu.Unlock()
71+
return nil
72+
}
73+
74+
// Get an item from the cache. Returns the item or nil, and a bool indicating
75+
// whether the key was found.
76+
func (c *cache) Get(k string) (interface{}, bool) {
77+
c.mu.RLock()
78+
item, found := c.items[k]
79+
if !found {
80+
c.mu.RUnlock()
81+
return nil, false
82+
}
83+
c.mu.RUnlock()
84+
return item.Object, true
85+
}
86+
87+
func (c *cache) get(k string) (interface{}, bool) {
88+
item, found := c.items[k]
89+
if !found {
90+
return nil, false
91+
}
92+
return item.Object, true
93+
}
94+
95+
// Delete an item from the cache. Does nothing if the key is not in the cache.
96+
func (c *cache) Delete(k string) {
97+
c.mu.Lock()
98+
delete(c.items, k)
99+
c.mu.Unlock()
100+
}
101+
102+
// Delete all expired items from the cache.
103+
func (c *cache) DeleteExpired() {
104+
now := time.Now().UnixNano()
105+
c.mu.Lock()
106+
for k, v := range c.items {
107+
if v.Expiration > 0 && now > v.Expiration {
108+
delete(c.items, k)
109+
}
110+
}
111+
c.mu.Unlock()
112+
}
113+
114+
type keyAndValue struct {
115+
key string
116+
value interface{}
117+
}
118+
119+
// OnExpired sets an (optional) function that is called when the cache expires
120+
func (c *cache) OnExpired(f func()) {
121+
c.onExpired = f
122+
}
123+
124+
// Returns the number of items in the cache, including expired items.
125+
func (c *cache) ItemCount() int {
126+
c.mu.RLock()
127+
n := len(c.items)
128+
c.mu.RUnlock()
129+
return n
130+
}
131+
132+
// Delete all items from the cache.
133+
func (c *cache) Flush() {
134+
c.mu.Lock()
135+
c.items = map[string]Item{}
136+
c.mu.Unlock()
137+
}
138+
139+
type janitor struct {
140+
Interval time.Duration
141+
stop chan bool
142+
}
143+
144+
// handleExpired is fired by the ticker and executes the onExpired function.
145+
func (c *cache) handleExpired() {
146+
if c.onExpired != nil {
147+
c.onExpired()
148+
}
149+
}
150+
151+
func (j *janitor) Run(c *cache) {
152+
ticker := time.NewTicker(j.Interval)
153+
for {
154+
select {
155+
case <-ticker.C:
156+
c.handleExpired()
157+
case <-j.stop:
158+
ticker.Stop()
159+
return
160+
}
161+
}
162+
}
163+
164+
func stopJanitor(c *Cache) {
165+
c.janitor.stop <- true
166+
}
167+
168+
func runJanitor(c *cache, ex time.Duration) {
169+
j := &janitor{
170+
Interval: ex,
171+
stop: make(chan bool, 1),
172+
}
173+
c.janitor = j
174+
go j.Run(c)
175+
}
176+
177+
func newCache(ex time.Duration, m map[string]Item) *cache {
178+
if ex <= 0 {
179+
ex = -1
180+
}
181+
c := &cache{
182+
expiration: ex,
183+
items: m,
184+
}
185+
return c
186+
}
187+
188+
func newCacheWithJanitor(ex time.Duration, m map[string]Item) *Cache {
189+
c := newCache(ex, m)
190+
// This trick ensures that the janitor goroutine (which--granted it
191+
// was enabled--is running DeleteExpired on c forever) does not keep
192+
// the returned C object from being garbage collected. When it is
193+
// garbage collected, the finalizer stops the janitor goroutine, after
194+
// which c can be collected.
195+
C := &Cache{c}
196+
if ex > 0 {
197+
runJanitor(c, ex)
198+
runtime.SetFinalizer(C, stopJanitor)
199+
}
200+
return C
201+
}
202+
203+
// New return a new cache with a given expiration duration. If the
204+
// expiration duration is less than 1 (i.e. No Expiration),
205+
// the items in the cache never expire (by default), and must be deleted
206+
// manually. The OnExpired callback method is ignored, too.
207+
func New(expiration time.Duration) *Cache {
208+
items := make(map[string]Item)
209+
return newCacheWithJanitor(expiration, items)
210+
}

0 commit comments

Comments
 (0)