利用 golang 中的 map 数据结构写一个带超时限制的键值存储组件
包括功能说明
基础的增删改查
- Set(k string, x interface{}, d time.Duration) : 新建立带时间限制的键值对,时间传0则不设置超时限制
- SetDefault(k string, x interface{}):新建一个无超时的键值对
- Add(k string, x interface{}, d time.Duration):新增一个不存在的 键值对(判断是否存在该Key)
- Replace(k string, x interface{}, d time.Duration):替换 已经存在 键的值
- Get(k string) (interface{}, bool):查询一个键值对
- GetWithExpiration(k string) (interface{}, time.Time, bool): 查询该键对应的 值,过期时间,是否存在 (不存在则不再返回超时时间和值)
- Delete(k string): 删除键值
- DeleteExpired(): 删除所有 超时的 键值
- Flush() : 清楚所有键值对。(包括超时和未超时的所有)
额外功能:
- OnEvicted(f func(string, interface{})):删除 键值对后 做一些回调事件
- Items() map[string]Item :返回所有未超时的 副本键值
- ItemCount() int: 返回对应所有的键值总数(包括超时的)
- Increment(k string, n int64): 对存在的键的值,做增加操作
- Decrement(k string, n int64):对存在的键的值,做减法操作
带超时时间限制
每个键值对可以设置对应的超时限制,如果没设置,或者设置为0,表示不做超时处理。
查询时,如果已经超过了设置的时间,则不再返回。
超时了的键值对,需要调用方法进行清理。
功能实现
map 并发安全问题:
在 go 语言中数据类型中的 map 并不是并发安全的,多次读读写写时会出现数据冲突问题。
然而在 go1.9 开始官方出现了 sync.Map 这个利器。
sync.Map 内部原理实现其实采用 lock free 利用两个数据结构 read (读写)和 dirty (删除), 以及 CAS (原子操作比较替换),状态替换等。实现了较为高效的且为并发安全的 map。
所以本篇也是主要利用 sync.Map 进行键值存储设计。
首先来看看数据结构的设计
type Item struct {
Object interface{} // value具体的值数据
Expiration int64 // 超时时间戳
}
type Cache struct {
*cache
// If this is confusing, see the comment at the bottom of New()
}
type janitor struct { // 用户组件的初始化启动停止
Interval time.Duration
stop chan bool
}
type cache struct {
defaultExpiration time.Duration // 默认超时时间设置
items map[string]Item
mu sync.RWMutex
ma sync.Map
onEvicted func(string, interface{})
janitor *janitor
}
对于超时限制的处理:
- Set时,建立超时字段存入即可,如下:
func (c *cache) Set(k string, x interface{}, d time.Duration) {
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano() // 加入超时限制,取未来(传入的超时时间段)某一点的时间戳
}
item := Item{
Object: x,
Expiration: e,
}
c.ma.Store(k, item) // 利用sync.map进行并发安全的存储
}
- Get 时进行函数内置的超时判断:
// Get an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found.
// 获取键值对应的值,内置判断是否超时
func (c *cache) Get(k string) (interface{}, bool) {
item, found := c.ma.Load(k)
if !found {
return nil, false
}
if item.(Item).Expiration > 0 { // 判断是否设置超时
if time.Now().UnixNano() > item.(Item).Expiration {
return nil, false // 判断超时即返回nil
}
}
return item.(Item).Object, true
}
代码展示
相关代码如下:
仅做参考,可自由摄取扩展等。如有雷同纯属借鉴~
package cache
import (
"encoding/gob"
"fmt"
"io"
"os"
"runtime"
"sync"
"time"
)
type Item struct {
Object interface{}
Expiration int64
}
// Returns true if the item has expired.
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}
const (
// For use with functions that take an expiration time.
NoExpiration time.Duration = -1
// For use with functions that take an expiration time. Equivalent to
// passing in the same expiration duration as was given to New() or
// NewFrom() when the cache was created (e.g. 5 minutes.)
DefaultExpiration time.Duration = 0
)
type Cache struct {
*cache
// If this is confusing, see the comment at the bottom of New()
}
type cache struct {
defaultExpiration time.Duration
items map[string]Item
mu sync.RWMutex
ma sync.Map
onEvicted func(string, interface{})
janitor *janitor
}
// Add an item to the cache, replacing any existing item. If the duration is 0
// (DefaultExpiration), the cache's default expiration time is used. If it is -1
// (NoExpiration), the item never expires.
func (c *cache) Set(k string, x interface{}, d time.Duration) {
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
item := Item{
Object: x,
Expiration: e,
}
c.ma.Store(k, item)
}
func (c *cache) set(k string, x interface{}, d time.Duration) {
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
item := Item{
Object: x,
Expiration: e,
}
c.ma.Store(k, item)
}
// Add an item to the cache, replacing any existing item, using the default
// expiration.
func (c *cache) SetDefault(k string, x interface{}) {
c.Set(k, x, DefaultExpiration)
}
// Add an item to the cache only if an item doesn't already exist for the given
// key, or if the existing item has expired. Returns an error otherwise.
// 新增一个不存在的 键值对(判断是否存在该Key)
func (c *cache) Add(k string, x interface{}, d time.Duration) error {
_, found := c.get(k)
if found {
return fmt.Errorf("Item %s already exists", k)
}
c.set(k, x, d)
return nil
}
// Set a new value for the cache key only if it already exists, and the existing
// item hasn't expired. Returns an error otherwise.
// 替换 已经存在的 键的值
func (c *cache) Replace(k string, x interface{}, d time.Duration) error {
_, found := c.get(k)
if !found {
return fmt.Errorf("Item %s doesn't exist", k)
}
c.set(k, x, d)
return nil
}
// Get an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found.
// 获取键值对应的值,内置判断是否超时
func (c *cache) Get(k string) (interface{}, bool) {
item, found := c.ma.Load(k)
if !found {
return nil, false
}
if item.(Item).Expiration > 0 {
if time.Now().UnixNano() > item.(Item).Expiration {
return nil, false
}
}
return item.(Item).Object, true
}
// GetWithExpiration returns an item and its expiration time from the cache.
// It returns the item or nil, the expiration time if one is set (if the item
// never expires a zero value for time.Time is returned), and a bool indicating
// whether the key was found.
// 返回该键对应值,过期时间,是否存在 (不存在则不再返回超时时间和值)
func (c *cache) GetWithExpiration(k string) (interface{}, time.Time, bool) {
// "Inlining" of get and Expired
item, found := c.ma.Load(k)
if !found {
return nil, time.Time{}, false
}
if item.(Item).Expiration > 0 {
if time.Now().UnixNano() > item.(Item).Expiration {
return nil, time.Time{}, false
}
// Return the item and the expiration time
return item.(Item).Object, time.Unix(0, item.(Item).Expiration), true
}
// If expiration <= 0 (i.e. no expiration time set) then return the item
// and a zeroed time.Time
return item.(Item).Object, time.Time{}, true
}
func (c *cache) get(k string) (interface{}, bool) {
item, found := c.ma.Load(k)
//item, found := c.items[k]
if !found {
return nil, false
}
// "Inlining" of Expired
if item.(Item).Expiration > 0 {
if time.Now().UnixNano() > item.(Item).Expiration {
return nil, false
}
}
return item.(Item).Object, true
}
// Increment an item of type int, int8, int16, int32, int64, uintptr, uint,
// uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the
// item's value is not an integer, if it was not found, or if it is not
// possible to increment it by n. To retrieve the incremented value, use one
// of the specialized methods, e.g. IncrementInt64.
func (c *cache) Increment(k string, n int64) error {
c.mu.Lock()
v, found := c.items[k]
if !found || v.Expired() {
c.mu.Unlock()
return fmt.Errorf("Item %s not found", k)
}
switch v.Object.(type) {
case int:
v.Object = v.Object.(int) + int(n)
case int8:
v.Object = v.Object.(int8) + int8(n)
case int16:
v.Object = v.Object.(int16) + int16(n)
case int32:
v.Object = v.Object.(int32) + int32(n)
case int64:
v.Object = v.Object.(int64) + n
case uint:
v.Object = v.Object.(uint) + uint(n)
case uintptr:
v.Object = v.Object.(uintptr) + uintptr(n)
case uint8:
v.Object = v.Object.(uint8) + uint8(n)
case uint16:
v.Object = v.Object.(uint16) + uint16(n)
case uint32:
v.Object = v.Object.(uint32) + uint32(n)
case uint64:
v.Object = v.Object.(uint64) + uint64(n)
case float32:
v.Object = v.Object.(float32) + float32(n)
case float64:
v.Object = v.Object.(float64) + float64(n)
default:
c.mu.Unlock()
return fmt.Errorf("The value for %s is not an integer", k)
}
c.items[k] = v
c.mu.Unlock()
return nil
}
// Decrement an item of type int, int8, int16, int32, int64, uintptr, uint,
// uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the
// item's value is not an integer, if it was not found, or if it is not
// possible to decrement it by n. To retrieve the decremented value, use one
// of the specialized methods, e.g. DecrementInt64.
func (c *cache) Decrement(k string, n int64) error {
// TODO: Implement Increment and Decrement more cleanly.
// (Cannot do Increment(k, n*-1) for uints.)
c.mu.Lock()
v, found := c.items[k]
if !found || v.Expired() {
c.mu.Unlock()
return fmt.Errorf("Item not found")
}
switch v.Object.(type) {
case int:
v.Object = v.Object.(int) - int(n)
case int8:
v.Object = v.Object.(int8) - int8(n)
case int16:
v.Object = v.Object.(int16) - int16(n)
case int32:
v.Object = v.Object.(int32) - int32(n)
case int64:
v.Object = v.Object.(int64) - n
case uint:
v.Object = v.Object.(uint) - uint(n)
case uintptr:
v.Object = v.Object.(uintptr) - uintptr(n)
case uint8:
v.Object = v.Object.(uint8) - uint8(n)
case uint16:
v.Object = v.Object.(uint16) - uint16(n)
case uint32:
v.Object = v.Object.(uint32) - uint32(n)
case uint64:
v.Object = v.Object.(uint64) - uint64(n)
case float32:
v.Object = v.Object.(float32) - float32(n)
case float64:
v.Object = v.Object.(float64) - float64(n)
default:
c.mu.Unlock()
return fmt.Errorf("The value for %s is not an integer", k)
}
c.items[k] = v
c.mu.Unlock()
return nil
}
// Delete an item from the cache. Does nothing if the key is not in the cache.
func (c *cache) Delete(k string) {
c.mu.Lock()
v, evicted := c.delete(k)
c.mu.Unlock()
if evicted {
c.onEvicted(k, v) // 给入具体参数,开始调用 func(string, interface{})
}
}
func (c *cache) delete(k string) (interface{}, bool) {
if c.onEvicted != nil {
if v, found := c.ma.Load(k); found {
c.ma.Delete(k)
return v.(Item).Object, true
}
}
c.ma.Delete(k)
return nil, false
}
type keyAndValue struct {
key string
value interface{}
}
// Delete all expired items from the cache.
// 删除所有 超时的 键值
func (c *cache) DeleteExpired() {
var evictedItems []keyAndValue
now := time.Now().UnixNano()
c.ma.Range(func(k, v interface{}) bool {
if v.(Item).Expiration > 0 && now > v.(Item).Expiration {
ov, evicted := c.delete(k.(string))
if evicted {
evictedItems = append(evictedItems, keyAndValue{k.(string), ov})
}
}
return true
})
for _, v := range evictedItems {
c.onEvicted(v.key, v.value)
}
}
// Sets an (optional) function that is called with the key and value when an
// item is evicted from the cache. (Including when it is deleted manually, but
// not when it is overwritten.) Set to nil to disable.
// 删除 键值对后 做一些回调事件
func (c *cache) OnEvicted(f func(string, interface{})) {
c.mu.Lock()
c.onEvicted = f
c.mu.Unlock()
}
// Write the cache's items (using Gob) to an io.Writer.
//
// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the
// documentation for NewFrom().)
func (c *cache) Save(w io.Writer) (err error) {
enc := gob.NewEncoder(w)
defer func() {
if x := recover(); x != nil {
err = fmt.Errorf("Error registering item types with Gob library")
}
}()
c.mu.RLock()
defer c.mu.RUnlock()
for _, v := range c.items {
gob.Register(v.Object)
}
err = enc.Encode(&c.items)
return
}
// Save the cache's items to the given filename, creating the file if it
// doesn't exist, and overwriting it if it does.
//
// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the
// documentation for NewFrom().)
func (c *cache) SaveFile(fname string) error {
fp, err := os.Create(fname)
if err != nil {
return err
}
err = c.Save(fp)
if err != nil {
fp.Close()
return err
}
return fp.Close()
}
// Add (Gob-serialized) cache items from an io.Reader, excluding any items with
// keys that already exist (and haven't expired) in the current cache.
//
// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the
// documentation for NewFrom().)
func (c *cache) Load(r io.Reader) error {
dec := gob.NewDecoder(r)
items := map[string]Item{}
err := dec.Decode(&items)
if err == nil {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range items {
ov, found := c.items[k]
if !found || ov.Expired() {
c.items[k] = v
}
}
}
return err
}
// Load and add cache items from the given filename, excluding any items with
// keys that already exist in the current cache.
//
// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the
// documentation for NewFrom().)
func (c *cache) LoadFile(fname string) error {
fp, err := os.Open(fname)
if err != nil {
return err
}
err = c.Load(fp)
if err != nil {
fp.Close()
return err
}
return fp.Close()
}
// Copies all unexpired items in the cache into a new map and returns it.
// 返回所有未超时的 副本键值
func (c *cache) Items() map[string]Item {
m := make(map[string]Item, c.ItemCount())
now := time.Now().UnixNano()
c.ma.Range(func(key, v interface{}) bool {
if v.(Item).Expiration > 0 {
if now <= v.(Item).Expiration {
m[key.(string)] = v.(Item)
}
}
return true
})
return m
}
// Returns the number of items in the cache. This may include items that have
// expired, but have not yet been cleaned up.
func (c *cache) ItemCount() int {
count := 0
c.ma.Range(func(key, _ interface{}) bool {
count++
return true
})
return count
}
// Delete all items from the cache.
func (c *cache) Flush() {
c.ma.Range(func(k, _ interface{}) bool {
c.ma.Delete(k)
return true
})
}
type janitor struct {
Interval time.Duration
stop chan bool
}
// 关于如何初始化并启动该组件函数:
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-j.stop:
ticker.Stop()
return
}
}
}
func stopJanitor(c *Cache) {
c.janitor.stop <- true
}
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),
}
c.janitor = j
go j.Run(c)
}
func newCache(de time.Duration, m map[string]Item) *cache {
if de == 0 {
de = -1
}
c := &cache{
defaultExpiration: de,
items: m,
}
return c
}
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
c := newCache(de, m)
// This trick ensures that the janitor goroutine (which--granted it
// was enabled--is running DeleteExpired on c forever) 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 can be collected.
C := &Cache{c}
if ci > 0 {
runJanitor(c, ci)
runtime.SetFinalizer(C, stopJanitor)
}
return C
}
// Return a new cache with a given default expiration duration and cleanup
// interval. If the expiration duration is less than one (or NoExpiration),
// the items in the cache never expire (by default), and must be deleted
// manually. If the cleanup interval is less than one, expired items are not
// deleted from the cache before calling c.DeleteExpired().
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
// Return a new cache with a given default expiration duration and cleanup
// interval. If the expiration duration is less than one (or NoExpiration),
// the items in the cache never expire (by default), and must be deleted
// manually. If the cleanup interval is less than one, expired items are not
// deleted from the cache before calling c.DeleteExpired().
//
// NewFrom() also accepts an items map which will serve as the underlying map
// for the cache. This is useful for starting from a deserialized cache
// (serialized using e.g. gob.Encode() on c.Items()), or passing in e.g.
// make(map[string]Item, 500) to improve startup performance when the cache
// is expected to reach a certain minimum size.
//
// Only the cache's methods synchronize access to this map, so it is not
// recommended to keep any references to the map around after creating a cache.
// If need be, the map can be accessed at a later point using c.Items() (subject
// to the same caveat.)
//
// Note regarding serialization: When using e.g. gob, make sure to
// gob.Register() the individual types stored in the cache before encoding a
// map retrieved with c.Items(), and to register those same types before
// decoding a blob containing an items map.
func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache {
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
测试代码:
package cache
import (
"bytes"
"io/ioutil"
"runtime"
"strconv"
"sync"
"testing"
"time"
)
type TestStruct struct {
Num int
Children []*TestStruct
}
func TestCache(t *testing.T) {
tc := New(DefaultExpiration, 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, DefaultExpiration)
tc.Set("b", "b", DefaultExpiration)
tc.Set("c", 3.5, DefaultExpiration)
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, DefaultExpiration)
tc.Set("b", 2, NoExpiration)
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)")
}
}
func TestNewFrom(t *testing.T) {
m := map[string]Item{
"a": Item{
Object: 1,
Expiration: 0,
},
"b": Item{
Object: 2,
Expiration: 0,
},
}
tc := NewFrom(DefaultExpiration, 0, m)
a, found := tc.Get("a")
if !found {
t.Fatal("Did not find a")
}
if a.(int) != 1 {
t.Fatal("a is not 1")
}
b, found := tc.Get("b")
if !found {
t.Fatal("Did not find b")
}
if b.(int) != 2 {
t.Fatal("b is not 2")
}
}
func TestStorePointerToStruct(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("foo", &TestStruct{Num: 1}, DefaultExpiration)
x, found := tc.Get("foo")
if !found {
t.Fatal("*TestStruct was not found for foo")
}
foo := x.(*TestStruct)
foo.Num++
y, found := tc.Get("foo")
if !found {
t.Fatal("*TestStruct was not found for foo (second time)")
}
bar := y.(*TestStruct)
if bar.Num != 2 {
t.Fatal("TestStruct.Num is not 2")
}
}
func TestIncrementWithInt(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tint", 1, DefaultExpiration)
err := tc.Increment("tint", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tint")
if !found {
t.Error("tint was not found")
}
if x.(int) != 3 {
t.Error("tint is not 3:", x)
}
}
func TestIncrementWithInt8(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tint8", int8(1), DefaultExpiration)
err := tc.Increment("tint8", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tint8")
if !found {
t.Error("tint8 was not found")
}
if x.(int8) != 3 {
t.Error("tint8 is not 3:", x)
}
}
func TestIncrementWithInt16(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tint16", int16(1), DefaultExpiration)
err := tc.Increment("tint16", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tint16")
if !found {
t.Error("tint16 was not found")
}
if x.(int16) != 3 {
t.Error("tint16 is not 3:", x)
}
}
func TestIncrementWithInt32(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tint32", int32(1), DefaultExpiration)
err := tc.Increment("tint32", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tint32")
if !found {
t.Error("tint32 was not found")
}
if x.(int32) != 3 {
t.Error("tint32 is not 3:", x)
}
}
func TestIncrementWithInt64(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tint64", int64(1), DefaultExpiration)
err := tc.Increment("tint64", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tint64")
if !found {
t.Error("tint64 was not found")
}
if x.(int64) != 3 {
t.Error("tint64 is not 3:", x)
}
}
func TestIncrementWithUint(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tuint", uint(1), DefaultExpiration)
err := tc.Increment("tuint", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tuint")
if !found {
t.Error("tuint was not found")
}
if x.(uint) != 3 {
t.Error("tuint is not 3:", x)
}
}
func TestIncrementWithUintptr(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tuintptr", uintptr(1), DefaultExpiration)
err := tc.Increment("tuintptr", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tuintptr")
if !found {
t.Error("tuintptr was not found")
}
if x.(uintptr) != 3 {
t.Error("tuintptr is not 3:", x)
}
}
func TestIncrementWithUint8(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tuint8", uint8(1), DefaultExpiration)
err := tc.Increment("tuint8", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tuint8")
if !found {
t.Error("tuint8 was not found")
}
if x.(uint8) != 3 {
t.Error("tuint8 is not 3:", x)
}
}
func TestIncrementWithUint16(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tuint16", uint16(1), DefaultExpiration)
err := tc.Increment("tuint16", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tuint16")
if !found {
t.Error("tuint16 was not found")
}
if x.(uint16) != 3 {
t.Error("tuint16 is not 3:", x)
}
}
func TestIncrementWithUint32(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tuint32", uint32(1), DefaultExpiration)
err := tc.Increment("tuint32", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tuint32")
if !found {
t.Error("tuint32 was not found")
}
if x.(uint32) != 3 {
t.Error("tuint32 is not 3:", x)
}
}
func TestIncrementWithUint64(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("tuint64", uint64(1), DefaultExpiration)
err := tc.Increment("tuint64", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("tuint64")
if !found {
t.Error("tuint64 was not found")
}
if x.(uint64) != 3 {
t.Error("tuint64 is not 3:", x)
}
}
func TestIncrementWithFloat32(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("float32", float32(1.5), DefaultExpiration)
err := tc.Increment("float32", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("float32")
if !found {
t.Error("float32 was not found")
}
if x.(float32) != 3.5 {
t.Error("float32 is not 3.5:", x)
}
}
func TestIncrementWithFloat64(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("float64", float64(1.5), DefaultExpiration)
err := tc.Increment("float64", 2)
if err != nil {
t.Error("Error incrementing:", err)
}
x, found := tc.Get("float64")
if !found {
t.Error("float64 was not found")
}
if x.(float64) != 3.5 {
t.Error("float64 is not 3.5:", x)
}
}
func TestDecrementWithInt(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("int", int(5), DefaultExpiration)
err := tc.Decrement("int", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("int")
if !found {
t.Error("int was not found")
}
if x.(int) != 3 {
t.Error("int is not 3:", x)
}
}
func TestDecrementWithInt8(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("int8", int8(5), DefaultExpiration)
err := tc.Decrement("int8", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("int8")
if !found {
t.Error("int8 was not found")
}
if x.(int8) != 3 {
t.Error("int8 is not 3:", x)
}
}
func TestDecrementWithInt16(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("int16", int16(5), DefaultExpiration)
err := tc.Decrement("int16", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("int16")
if !found {
t.Error("int16 was not found")
}
if x.(int16) != 3 {
t.Error("int16 is not 3:", x)
}
}
func TestDecrementWithInt32(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("int32", int32(5), DefaultExpiration)
err := tc.Decrement("int32", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("int32")
if !found {
t.Error("int32 was not found")
}
if x.(int32) != 3 {
t.Error("int32 is not 3:", x)
}
}
func TestDecrementWithInt64(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("int64", int64(5), DefaultExpiration)
err := tc.Decrement("int64", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("int64")
if !found {
t.Error("int64 was not found")
}
if x.(int64) != 3 {
t.Error("int64 is not 3:", x)
}
}
func TestDecrementWithUint(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint", uint(5), DefaultExpiration)
err := tc.Decrement("uint", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("uint")
if !found {
t.Error("uint was not found")
}
if x.(uint) != 3 {
t.Error("uint is not 3:", x)
}
}
func TestDecrementWithUintptr(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uintptr", uintptr(5), DefaultExpiration)
err := tc.Decrement("uintptr", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("uintptr")
if !found {
t.Error("uintptr was not found")
}
if x.(uintptr) != 3 {
t.Error("uintptr is not 3:", x)
}
}
func TestDecrementWithUint8(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint8", uint8(5), DefaultExpiration)
err := tc.Decrement("uint8", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("uint8")
if !found {
t.Error("uint8 was not found")
}
if x.(uint8) != 3 {
t.Error("uint8 is not 3:", x)
}
}
func TestDecrementWithUint16(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint16", uint16(5), DefaultExpiration)
err := tc.Decrement("uint16", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("uint16")
if !found {
t.Error("uint16 was not found")
}
if x.(uint16) != 3 {
t.Error("uint16 is not 3:", x)
}
}
func TestDecrementWithUint32(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint32", uint32(5), DefaultExpiration)
err := tc.Decrement("uint32", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("uint32")
if !found {
t.Error("uint32 was not found")
}
if x.(uint32) != 3 {
t.Error("uint32 is not 3:", x)
}
}
func TestDecrementWithUint64(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint64", uint64(5), DefaultExpiration)
err := tc.Decrement("uint64", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("uint64")
if !found {
t.Error("uint64 was not found")
}
if x.(uint64) != 3 {
t.Error("uint64 is not 3:", x)
}
}
func TestDecrementWithFloat32(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("float32", float32(5.5), DefaultExpiration)
err := tc.Decrement("float32", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("float32")
if !found {
t.Error("float32 was not found")
}
if x.(float32) != 3.5 {
t.Error("float32 is not 3:", x)
}
}
func TestDecrementWithFloat64(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("float64", float64(5.5), DefaultExpiration)
err := tc.Decrement("float64", 2)
if err != nil {
t.Error("Error decrementing:", err)
}
x, found := tc.Get("float64")
if !found {
t.Error("float64 was not found")
}
if x.(float64) != 3.5 {
t.Error("float64 is not 3:", x)
}
}
func TestAdd(t *testing.T) {
tc := New(DefaultExpiration, 0)
err := tc.Add("foo", "bar", DefaultExpiration)
if err != nil {
t.Error("Couldn't add foo even though it shouldn't exist")
}
err = tc.Add("foo", "baz", DefaultExpiration)
if err == nil {
t.Error("Successfully added another foo when it should have returned an error")
}
}
func TestReplace(t *testing.T) {
tc := New(DefaultExpiration, 0)
err := tc.Replace("foo", "bar", DefaultExpiration)
if err == nil {
t.Error("Replaced foo when it shouldn't exist")
}
tc.Set("foo", "bar", DefaultExpiration)
err = tc.Replace("foo", "bar", DefaultExpiration)
if err != nil {
t.Error("Couldn't replace existing key foo")
}
}
func TestDelete(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("foo", "bar", DefaultExpiration)
tc.Delete("foo")
x, found := tc.Get("foo")
if found {
t.Error("foo was found, but it should have been deleted")
}
if x != nil {
t.Error("x is not nil:", x)
}
}
func TestItemCount(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("foo", "1", DefaultExpiration)
tc.Set("bar", "2", DefaultExpiration)
tc.Set("baz", "3", DefaultExpiration)
if n := tc.ItemCount(); n != 3 {
t.Errorf("Item count is not 3: %d", n)
}
}
func TestFlush(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("foo", "bar", DefaultExpiration)
tc.Set("baz", "yes", DefaultExpiration)
tc.Flush()
x, found := tc.Get("foo")
if found {
t.Error("foo was found, but it should have been deleted")
}
if x != nil {
t.Error("x is not nil:", x)
}
x, found = tc.Get("baz")
if found {
t.Error("baz was found, but it should have been deleted")
}
if x != nil {
t.Error("x is not nil:", x)
}
}
func TestIncrementOverflowInt(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("int8", int8(127), DefaultExpiration)
err := tc.Increment("int8", 1)
if err != nil {
t.Error("Error incrementing int8:", err)
}
x, _ := tc.Get("int8")
int8 := x.(int8)
if int8 != -128 {
t.Error("int8 did not overflow as expected; value:", int8)
}
}
func TestIncrementOverflowUint(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint8", uint8(255), DefaultExpiration)
err := tc.Increment("uint8", 1)
if err != nil {
t.Error("Error incrementing int8:", err)
}
x, _ := tc.Get("uint8")
uint8 := x.(uint8)
if uint8 != 0 {
t.Error("uint8 did not overflow as expected; value:", uint8)
}
}
func TestDecrementUnderflowUint(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("uint8", uint8(0), DefaultExpiration)
err := tc.Decrement("uint8", 1)
if err != nil {
t.Error("Error decrementing int8:", err)
}
x, _ := tc.Get("uint8")
uint8 := x.(uint8)
if uint8 != 255 {
t.Error("uint8 did not underflow as expected; value:", uint8)
}
}
func TestOnEvicted(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Set("foo", 3, DefaultExpiration)
if tc.onEvicted != nil {
t.Fatal("tc.onEvicted is not nil")
}
works := false
tc.OnEvicted(func(k string, v interface{}) {
if k == "foo" && v.(int) == 3 {
works = true
}
tc.Set("bar", 4, DefaultExpiration)
})
tc.Delete("foo")
x, _ := tc.Get("bar")
if !works {
t.Error("works bool not true")
}
if x.(int) != 4 {
t.Error("bar was not 4")
}
}
func TestCacheSerialization(t *testing.T) {
tc := New(DefaultExpiration, 0)
testFillAndSerialize(t, tc)
// Check if gob.Register behaves properly even after multiple gob.Register
// on c.Items (many of which will be the same type)
testFillAndSerialize(t, tc)
}
func testFillAndSerialize(t *testing.T, tc *Cache) {
tc.Set("a", "a", DefaultExpiration)
tc.Set("b", "b", DefaultExpiration)
tc.Set("c", "c", DefaultExpiration)
tc.Set("expired", "foo", 1*time.Millisecond)
tc.Set("*struct", &TestStruct{Num: 1}, DefaultExpiration)
tc.Set("[]struct", []TestStruct{
{Num: 2},
{Num: 3},
}, DefaultExpiration)
tc.Set("[]*struct", []*TestStruct{
&TestStruct{Num: 4},
&TestStruct{Num: 5},
}, DefaultExpiration)
tc.Set("structception", &TestStruct{
Num: 42,
Children: []*TestStruct{
&TestStruct{Num: 6174},
&TestStruct{Num: 4716},
},
}, DefaultExpiration)
fp := &bytes.Buffer{}
err := tc.Save(fp)
if err != nil {
t.Fatal("Couldn't save cache to fp:", err)
}
oc := New(DefaultExpiration, 0)
err = oc.Load(fp)
if err != nil {
t.Fatal("Couldn't load cache from fp:", err)
}
a, found := oc.Get("a")
if !found {
t.Error("a was not found")
}
if a.(string) != "a" {
t.Error("a is not a")
}
b, found := oc.Get("b")
if !found {
t.Error("b was not found")
}
if b.(string) != "b" {
t.Error("b is not b")
}
c, found := oc.Get("c")
if !found {
t.Error("c was not found")
}
if c.(string) != "c" {
t.Error("c is not c")
}
<-time.After(5 * time.Millisecond)
_, found = oc.Get("expired")
if found {
t.Error("expired was found")
}
s1, found := oc.Get("*struct")
if !found {
t.Error("*struct was not found")
}
if s1.(*TestStruct).Num != 1 {
t.Error("*struct.Num is not 1")
}
s2, found := oc.Get("[]struct")
if !found {
t.Error("[]struct was not found")
}
s2r := s2.([]TestStruct)
if len(s2r) != 2 {
t.Error("Length of s2r is not 2")
}
if s2r[0].Num != 2 {
t.Error("s2r[0].Num is not 2")
}
if s2r[1].Num != 3 {
t.Error("s2r[1].Num is not 3")
}
s3, found := oc.get("[]*struct")
if !found {
t.Error("[]*struct was not found")
}
s3r := s3.([]*TestStruct)
if len(s3r) != 2 {
t.Error("Length of s3r is not 2")
}
if s3r[0].Num != 4 {
t.Error("s3r[0].Num is not 4")
}
if s3r[1].Num != 5 {
t.Error("s3r[1].Num is not 5")
}
s4, found := oc.get("structception")
if !found {
t.Error("structception was not found")
}
s4r := s4.(*TestStruct)
if len(s4r.Children) != 2 {
t.Error("Length of s4r.Children is not 2")
}
if s4r.Children[0].Num != 6174 {
t.Error("s4r.Children[0].Num is not 6174")
}
if s4r.Children[1].Num != 4716 {
t.Error("s4r.Children[1].Num is not 4716")
}
}
func TestFileSerialization(t *testing.T) {
tc := New(DefaultExpiration, 0)
tc.Add("a", "a", DefaultExpiration)
tc.Add("b", "b", DefaultExpiration)
f, err := ioutil.TempFile("", "go-cache-cache.dat")
if err != nil {
t.Fatal("Couldn't create cache file:", err)
}
fname := f.Name()
f.Close()
tc.SaveFile(fname)
oc := New(DefaultExpiration, 0)
oc.Add("a", "aa", 0) // this should not be overwritten
err = oc.LoadFile(fname)
if err != nil {
t.Error(err)
}
a, found := oc.Get("a")
if !found {
t.Error("a was not found")
}
astr := a.(string)
if astr != "aa" {
if astr == "a" {
t.Error("a was overwritten")
} else {
t.Error("a is not aa")
}
}
b, found := oc.Get("b")
if !found {
t.Error("b was not found")
}
if b.(string) != "b" {
t.Error("b is not b")
}
}
func TestSerializeUnserializable(t *testing.T) {
tc := New(DefaultExpiration, 0)
ch := make(chan bool, 1)
ch <- true
tc.Set("chan", ch, DefaultExpiration)
fp := &bytes.Buffer{}
err := tc.Save(fp) // this should fail gracefully
if err.Error() != "gob NewTypeObject can't handle type: chan bool" {
t.Error("Error from Save was not gob NewTypeObject can't handle type chan bool:", err)
}
}
func BenchmarkCacheGetExpiring(b *testing.B) {
benchmarkCacheGet(b, 5*time.Minute)
}
func BenchmarkCacheGetNotExpiring(b *testing.B) {
benchmarkCacheGet(b, NoExpiration)
}
func benchmarkCacheGet(b *testing.B, exp time.Duration) {
b.StopTimer()
tc := New(exp, 0)
tc.Set("foo", "bar", DefaultExpiration)
b.StartTimer()
for i := 0; i < b.N; i++ {
tc.Get("foo")
}
}
func BenchmarkRWMutexMapGet(b *testing.B) {
b.StopTimer()
m := map[string]string{
"foo": "bar",
}
mu := sync.RWMutex{}
b.StartTimer()
for i := 0; i < b.N; i++ {
mu.RLock()
_, _ = m["foo"]
mu.RUnlock()
}
}
func BenchmarkRWMutexInterfaceMapGetStruct(b *testing.B) {
b.StopTimer()
s := struct{ name string }{name: "foo"}
m := map[interface{}]string{
s: "bar",
}
mu := sync.RWMutex{}
b.StartTimer()
for i := 0; i < b.N; i++ {
mu.RLock()
_, _ = m[s]
mu.RUnlock()
}
}
func BenchmarkRWMutexInterfaceMapGetString(b *testing.B) {
b.StopTimer()
m := map[interface{}]string{
"foo": "bar",
}
mu := sync.RWMutex{}
b.StartTimer()
for i := 0; i < b.N; i++ {
mu.RLock()
_, _ = m["foo"]
mu.RUnlock()
}
}
func BenchmarkCacheGetConcurrentExpiring(b *testing.B) {
benchmarkCacheGetConcurrent(b, 5*time.Minute)
}
func BenchmarkCacheGetConcurrentNotExpiring(b *testing.B) {
benchmarkCacheGetConcurrent(b, NoExpiration)
}
func benchmarkCacheGetConcurrent(b *testing.B, exp time.Duration) {
b.StopTimer()
tc := New(exp, 0)
tc.Set("foo", "bar", DefaultExpiration)
wg := new(sync.WaitGroup)
workers := runtime.NumCPU()
each := b.N / workers
wg.Add(workers)
b.StartTimer()
for i := 0; i < workers; i++ {
go func() {
for j := 0; j < each; j++ {
tc.Get("foo")
}
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkRWMutexMapGetConcurrent(b *testing.B) {
b.StopTimer()
m := map[string]string{
"foo": "bar",
}
mu := sync.RWMutex{}
wg := new(sync.WaitGroup)
workers := runtime.NumCPU()
each := b.N / workers
wg.Add(workers)
b.StartTimer()
for i := 0; i < workers; i++ {
go func() {
for j := 0; j < each; j++ {
mu.RLock()
_, _ = m["foo"]
mu.RUnlock()
}
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkCacheGetManyConcurrentExpiring(b *testing.B) {
benchmarkCacheGetManyConcurrent(b, 5*time.Minute)
}
func BenchmarkCacheGetManyConcurrentNotExpiring(b *testing.B) {
benchmarkCacheGetManyConcurrent(b, NoExpiration)
}
func benchmarkCacheGetManyConcurrent(b *testing.B, exp time.Duration) {
// This is the same as BenchmarkCacheGetConcurrent, but its result
// can be compared against BenchmarkShardedCacheGetManyConcurrent
// in sharded_test.go.
b.StopTimer()
n := 10000
tc := New(exp, 0)
keys := make([]string, n)
for i := 0; i < n; i++ {
k := "foo" + strconv.Itoa(i)
keys[i] = k
tc.Set(k, "bar", DefaultExpiration)
}
each := b.N / n
wg := new(sync.WaitGroup)
wg.Add(n)
for _, v := range keys {
go func(k string) {
for j := 0; j < each; j++ {
tc.Get(k)
}
wg.Done()
}(v)
}
b.StartTimer()
wg.Wait()
}
func BenchmarkCacheSetExpiring(b *testing.B) {
benchmarkCacheSet(b, 5*time.Minute)
}
func BenchmarkCacheSetNotExpiring(b *testing.B) {
benchmarkCacheSet(b, NoExpiration)
}
func benchmarkCacheSet(b *testing.B, exp time.Duration) {
b.StopTimer()
tc := New(exp, 0)
b.StartTimer()
for i := 0; i < b.N; i++ {
tc.Set("foo", "bar", DefaultExpiration)
}
}
func BenchmarkRWMutexMapSet(b *testing.B) {
b.StopTimer()
m := map[string]string{}
mu := sync.RWMutex{}
b.StartTimer()
for i := 0; i < b.N; i++ {
mu.Lock()
m["foo"] = "bar"
mu.Unlock()
}
}
func BenchmarkCacheSetDelete(b *testing.B) {
b.StopTimer()
tc := New(DefaultExpiration, 0)
b.StartTimer()
for i := 0; i < b.N; i++ {
tc.Set("foo", "bar", DefaultExpiration)
tc.Delete("foo")
}
}
func BenchmarkRWMutexMapSetDelete(b *testing.B) {
b.StopTimer()
m := map[string]string{}
mu := sync.RWMutex{}
b.StartTimer()
for i := 0; i < b.N; i++ {
mu.Lock()
m["foo"] = "bar"
mu.Unlock()
mu.Lock()
delete(m, "foo")
mu.Unlock()
}
}
func BenchmarkCacheSetDeleteSingleLock(b *testing.B) {
b.StopTimer()
tc := New(DefaultExpiration, 0)
b.StartTimer()
for i := 0; i < b.N; i++ {
tc.mu.Lock()
tc.set("foo", "bar", DefaultExpiration)
tc.delete("foo")
tc.mu.Unlock()
}
}
func BenchmarkRWMutexMapSetDeleteSingleLock(b *testing.B) {
b.StopTimer()
m := map[string]string{}
mu := sync.RWMutex{}
b.StartTimer()
for i := 0; i < b.N; i++ {
mu.Lock()
m["foo"] = "bar"
delete(m, "foo")
mu.Unlock()
}
}
func BenchmarkDeleteExpiredLoop(b *testing.B) {
b.StopTimer()
tc := New(5*time.Minute, 0)
tc.mu.Lock()
for i := 0; i < 100000; i++ {
tc.set(strconv.Itoa(i), "bar", DefaultExpiration)
}
tc.mu.Unlock()
b.StartTimer()
for i := 0; i < b.N; i++ {
tc.DeleteExpired()
}
}
func TestGetWithExpiration(t *testing.T) {
tc := New(DefaultExpiration, 0)
a, expiration, found := tc.GetWithExpiration("a")
if found || a != nil || !expiration.IsZero() {
t.Error("Getting A found value that shouldn't exist:", a)
}
b, expiration, found := tc.GetWithExpiration("b")
if found || b != nil || !expiration.IsZero() {
t.Error("Getting B found value that shouldn't exist:", b)
}
c, expiration, found := tc.GetWithExpiration("c")
if found || c != nil || !expiration.IsZero() {
t.Error("Getting C found value that shouldn't exist:", c)
}
tc.Set("a", 1, DefaultExpiration)
tc.Set("b", "b", DefaultExpiration)
tc.Set("c", 3.5, DefaultExpiration)
tc.Set("d", 1, NoExpiration)
tc.Set("e", 1, 50*time.Millisecond)
x, expiration, found := tc.GetWithExpiration("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)
}
if !expiration.IsZero() {
t.Error("expiration for a is not a zeroed time")
}
x, expiration, found = tc.GetWithExpiration("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)
}
if !expiration.IsZero() {
t.Error("expiration for b is not a zeroed time")
}
x, expiration, found = tc.GetWithExpiration("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)
}
if !expiration.IsZero() {
t.Error("expiration for c is not a zeroed time")
}
x, expiration, found = tc.GetWithExpiration("d")
if !found {
t.Error("d was not found while getting d2")
}
if x == nil {
t.Error("x for d is nil")
} else if d2 := x.(int); d2+2 != 3 {
t.Error("d (which should be 1) plus 2 does not equal 3; value:", d2)
}
if !expiration.IsZero() {
t.Error("expiration for d is not a zeroed time")
}
x, expiration, found = tc.GetWithExpiration("e")
if !found {
t.Error("e was not found while getting e2")
}
if x == nil {
t.Error("x for e is nil")
} else if e2 := x.(int); e2+2 != 3 {
t.Error("e (which should be 1) plus 2 does not equal 3; value:", e2)
}
if expiration.UnixNano() != tc.items["e"].Expiration {
t.Error("expiration for e is not the correct time")
}
if expiration.UnixNano() < time.Now().UnixNano() {
t.Error("expiration for e is in the past")
}
}