Initial commit
commit
3088a9aad8
@ -0,0 +1,7 @@
|
||||
_test*
|
||||
src/_test*
|
||||
_obj/
|
||||
*.5
|
||||
*.6
|
||||
*.8
|
||||
*.out
|
@ -0,0 +1,7 @@
|
||||
include $(GOROOT)/src/Make.inc
|
||||
|
||||
TARG=github.com/pmylund/go-cache
|
||||
GOFILES=\
|
||||
cache.go\
|
||||
|
||||
include $(GOROOT)/src/Make.pkg
|
@ -0,0 +1,216 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache is an in-memory cache similar to memcached that is suitable for applications
|
||||
// running on a single machine. Any object can be stored, for a given duration or forever,
|
||||
// and the cache can be used safely by multiple goroutines.
|
||||
//
|
||||
// Installation:
|
||||
// goinstall github.com/pmylund/go-cache
|
||||
//
|
||||
// Usage:
|
||||
// // Create a cache with a default expiration time of 5 minutes, and which purges
|
||||
// // expired items every 30 seconds
|
||||
// c := cache.New(5*time.Minute, 30*time.Second)
|
||||
//
|
||||
// // Set the value of the key "foo" to "bar", with the default expiration time
|
||||
// c.Set("foo", "bar", 0)
|
||||
//
|
||||
// // Set the value of the key "baz" to "yes", with no expiration time (the item
|
||||
// // won't be removed until it is re-set, or removed using c.Delete("baz")
|
||||
// c.Set("baz", "yes", -1)
|
||||
//
|
||||
// // Get the string associated with the key "foo" from the cache
|
||||
// foo, found := c.Get("foo")
|
||||
// if found {
|
||||
// fmt.Println(foo)
|
||||
// }
|
||||
//
|
||||
// // Since Go is statically typed, and cache values can be anything, type assertion
|
||||
// // is needed when values are being passed to functions that don't take arbitrary types,
|
||||
// // (i.e. interface{}). The simplest way to do this for values which will only be passed
|
||||
// // once is:
|
||||
// foo, found := c.Get("foo")
|
||||
// if found {
|
||||
// MyFunction(foo.(string))
|
||||
// }
|
||||
//
|
||||
// // This gets tedious if the value is used several times in the same function. You
|
||||
// // might do either of the following instead:
|
||||
// if x, found := c.Get("foo"); found {
|
||||
// foo := x.(string)
|
||||
// ...
|
||||
// }
|
||||
// // or
|
||||
// var foo string
|
||||
// if x, found := c.Get("foo"); found {
|
||||
// foo = x.(string)
|
||||
// }
|
||||
// // foo can then be passed around freely as a string
|
||||
//
|
||||
// // Want performance? Store pointers!
|
||||
// c.Set("foo", &MyStruct, 0)
|
||||
// if x, found := c.Get("foo"); found {
|
||||
// foo := x.(*MyStruct)
|
||||
// ...
|
||||
// }
|
||||
|
||||
|
||||
type Cache struct {
|
||||
*cache
|
||||
// If this is confusing, see the comment at the bottom of the New() function
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
DefaultExpiration time.Duration
|
||||
Items map[string]*Item
|
||||
mu *sync.Mutex
|
||||
janitor *janitor
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Object interface{}
|
||||
Expires bool
|
||||
Expiration *time.Time
|
||||
}
|
||||
|
||||
type janitor struct {
|
||||
Interval time.Duration
|
||||
stop chan bool
|
||||
}
|
||||
|
||||
// Adds an item to the cache. If the duration is 0, the cache's default expiration time
|
||||
// is used. If it is -1, the item never expires.
|
||||
func (c *cache) Set(key string, x interface{}, d time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var e *time.Time
|
||||
expires := true
|
||||
if d == 0 {
|
||||
d = c.DefaultExpiration
|
||||
}
|
||||
if d == -1 {
|
||||
expires = false
|
||||
} else {
|
||||
t := time.Now().Add(d)
|
||||
e = &t
|
||||
}
|
||||
c.Items[key] = &Item{
|
||||
Object: x,
|
||||
Expires: expires,
|
||||
Expiration: e,
|
||||
}
|
||||
}
|
||||
|
||||
// Gets an item from the cache.
|
||||
func (c *cache) Get(key string) (interface{}, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
item, found := c.Items[key]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
if item.Expired() {
|
||||
delete(c.Items, key)
|
||||
return nil, false
|
||||
}
|
||||
return item.Object, true
|
||||
}
|
||||
|
||||
// Deletes an item from the cache. Does nothing if the item does not exist in the cache.
|
||||
func (c *cache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
delete(c.Items, key)
|
||||
}
|
||||
|
||||
// Deletes all expired items from the cache.
|
||||
func (c *cache) DeleteExpired() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for k, v := range c.Items {
|
||||
if v.Expired() {
|
||||
delete(c.Items, k)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Deletes all items in the cache
|
||||
func (c *cache) Purge() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.Items = map[string]*Item{}
|
||||
}
|
||||
|
||||
// Returns true if the item has expired.
|
||||
func (i *Item) Expired() bool {
|
||||
if i.Expiration == nil {
|
||||
return false
|
||||
}
|
||||
return i.Expiration.Before(time.Now())
|
||||
}
|
||||
|
||||
|
||||
func (j *janitor) Run(c *cache) {
|
||||
j.stop = make(chan bool)
|
||||
tick := time.Tick(j.Interval)
|
||||
for {
|
||||
select {
|
||||
case <-tick:
|
||||
c.DeleteExpired()
|
||||
case <-j.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *janitor) Stop() {
|
||||
j.stop <- true
|
||||
}
|
||||
|
||||
func stopJanitor(c *Cache) {
|
||||
c.janitor.Stop()
|
||||
}
|
||||
|
||||
// Returns a new cache with a given default expiration duration and default cleanup
|
||||
// interval. If the expiration duration is less than 1, the items in the cache never expire
|
||||
// and have to be deleted manually. If the cleanup interval is less than one, expired
|
||||
// items are not deleted from the cache before their next lookup or before calling
|
||||
// DeleteExpired.
|
||||
func New(de, ci time.Duration) *Cache {
|
||||
if de == 0 {
|
||||
de = -1
|
||||
}
|
||||
c := &cache{
|
||||
DefaultExpiration: de,
|
||||
Items: map[string]*Item{},
|
||||
mu: &sync.Mutex{},
|
||||
}
|
||||
if ci > 0 {
|
||||
j := &janitor{
|
||||
Interval: ci,
|
||||
}
|
||||
c.janitor = j
|
||||
go j.Run(c)
|
||||
}
|
||||
// This trick ensures that the janitor goroutine (which--granted it was enabled--is
|
||||
// running DeleteExpired on c forever, if it was enabled) does not keep the returned C
|
||||
// object from being garbage collected. When it is garbage collected, the finalizer stops
|
||||
// the janitor goroutine, after which c is collected.
|
||||
C := &Cache{c}
|
||||
if ci > 0 {
|
||||
runtime.SetFinalizer(C, stopJanitor)
|
||||
}
|
||||
return C
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
tc := New(0, 0)
|
||||
|
||||
a, found := tc.Get("a")
|
||||
if found || a != nil {
|
||||
t.Error("Getting A found value that shouldn't exist:", a)
|
||||
}
|
||||
|
||||
b, found := tc.Get("b")
|
||||
if found || b != nil {
|
||||
t.Error("Getting B found value that shouldn't exist:", b)
|
||||
}
|
||||
|
||||
c, found := tc.Get("c")
|
||||
if found || c != nil {
|
||||
t.Error("Getting C found value that shouldn't exist:", c)
|
||||
}
|
||||
|
||||
tc.Set("a", 1, 0)
|
||||
tc.Set("b", "b", 0)
|
||||
tc.Set("c", 3.5, 0)
|
||||
|
||||
x, found := tc.Get("a")
|
||||
if !found {
|
||||
t.Error("a was not found while getting a2")
|
||||
}
|
||||
if x == nil {
|
||||
t.Error("x for a is nil")
|
||||
} else if a2 := x.(int); a2 + 2 != 3 {
|
||||
t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2)
|
||||
}
|
||||
|
||||
x, found = tc.Get("b")
|
||||
if !found {
|
||||
t.Error("b was not found while getting b2")
|
||||
}
|
||||
if x == nil {
|
||||
t.Error("x for b is nil")
|
||||
} else if b2 := x.(string); b2 + "B" != "bB" {
|
||||
t.Error("b2 (which should be b) plus B does not equal bB; value:", b2)
|
||||
}
|
||||
|
||||
x, found = tc.Get("c")
|
||||
if !found {
|
||||
t.Error("c was not found while getting c2")
|
||||
}
|
||||
if x == nil {
|
||||
t.Error("x for c is nil")
|
||||
} else if c2 := x.(float64); c2 + 1.2 != 4.7 {
|
||||
t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheTimes(t *testing.T) {
|
||||
var found bool
|
||||
|
||||
tc := New(50*time.Millisecond, 1*time.Millisecond)
|
||||
tc.Set("a", 1, 0)
|
||||
tc.Set("b", 2, -1)
|
||||
tc.Set("c", 3, 20*time.Millisecond)
|
||||
tc.Set("d", 4, 70*time.Millisecond)
|
||||
|
||||
<-time.After(25*time.Millisecond)
|
||||
_, found = tc.Get("c")
|
||||
if found {
|
||||
t.Error("Found c when it should have been automatically deleted")
|
||||
}
|
||||
|
||||
<-time.After(30*time.Millisecond)
|
||||
_, found = tc.Get("a")
|
||||
if found {
|
||||
t.Error("Found a when it should have been automatically deleted")
|
||||
}
|
||||
|
||||
_, found = tc.Get("b")
|
||||
if !found {
|
||||
t.Error("Did not find b even though it was set to never expire")
|
||||
}
|
||||
|
||||
_, found = tc.Get("d")
|
||||
if !found {
|
||||
t.Error("Did not find d even though it was set to expire later than the default")
|
||||
}
|
||||
|
||||
<-time.After(20*time.Millisecond)
|
||||
_, found = tc.Get("d")
|
||||
if found {
|
||||
t.Error("Found d when it should have been automatically deleted (later than the default)")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue