Inspired by the Laravel Cache implementation, this package provides a store agnostic caching system via an expressive and unified interface that allows for an abstraction layer between different data store drivers and your application. This enables for each store to be used interchangeably without any code changes other than the programmatic configuration of the desired store(s).
To start using this package in your application simply run:go get -u github.com/alejandro-carstens/gocache
This package supports 3 backends out of the box: Redis, Memcached and Local (via go-cache). Each store has a specific configuration whose parameters can be easily referenced in the following GoDoc sections:
In order to new up a cache implementation simply call gocache.New
with the desired configuration and encoder:
// Redis
cache, err := gocache.New(&gocache.RedisConfig{
Prefix: "gocache:",
Addr: "localhost:6379",
}, encoder.JSON{})
// handle err
// Memcache
cache, err := gocache.New(&gocache.MemcacheConfig{
Prefix: "gocache:",
Servers: []string{"127.0.0.1:11211"},
}, encoder.Msgpack{})
// handle err
// Local
cache, err := gocache.New(&gocache.LocalConfig{
Prefix: "gocache:",
DefaultInterval: time.Second,
}, encoder.JSON{})
// handle err
All methods including the prefix Get
are used to retrieve items from the cache. If an item does not exist in the cache for the given key an error of type gocache.ErrNotFound
will be raised. Please see the following examples:
v, err := cache.GetFloat32("temperature")
// handle err
v, err := cache.GetFloat64("height")
// handle err
v, err := cache.GetInt("score")
// handle err
v, err := cache.GetInt64("counter")
// handle err
v, err := cache.GetString("username")
// handle err
v, err := cache.GetUint64("id")
// handle err
// GetBool will return false in the event of the cache entry being
// the string 'false', empty string, boolean false, string '0' or
// the number 0. The value v will be true otherwise.
v, err := cache.GetBool("active")
// handle err
// Get any type e.g. Movie{Name string, Views int64}
var m Movie
err := cache.Get("e.t.", &m)
// handle err
// Handle missed entry for key
v, err := cache.GetString("entry-not-found-key")
if errors.Is(gocache.ErrNotFound, err) {
// handle err
}
The method Many
is also exposed in order to retrieve multiple cache records with one call. The results of the Many
invocation will be returned in a map of gocache.Item instances keyed by the retrieved cached entries keys. Please see the example below:
items, err := cache.Many("string", "uint64", "int", "int64", "float64", "float32", "any", "bool")
// handle err
for key, item := range items {
switch key:
case "string":
v, err := item.String()
// handle err
case "uint64":
v, err := item.Uint64()
// handle err
case "int":
v, err := item.Int()
// handle err
case "int64":
v, err := item.Int64()
// handle err
case "float64":
v, err := item.Float64()
// handle err
case "float32":
v, err := item.Float32()
// handle err
case "any":
var m Movie
err := item.Unmarshal(&m)
// handle err
case "bool":
// Bool will return false in the event of the cache entry being
// the string 'false', empty string, boolean false, string '0'
// or the number 0. The value v will be true otherwise.
v, err := item.Bool()
// handle err
}
}
You can use the Put
method to store items in the cache with a specified time to live:
err := cache.Put("key", "value", 10 * time.Second)
// handle err
// You can store any value
err := cache.Put("most_watched_movie", &Movie{
Name: "Avatar",
Views: 100,
}, 60 * time.Minute)
// handle err
To atomically add an entry to the cache if the key for the given entry does not exist you can use Add
:
added, err := cache.Add("key", 2, time.Minute)
// handle err
if !added {
// do something
}
To store a value indefinitely (without expiration) simply use the method Forever
:
err := cache.Forever("key", "value")
// handle err
To store many values at once you can use PutMany
:
var entries = []gocache.Entry{
{
Key: "string",
Value: "whatever",
Duration: time.Minute,
},
{
Key: "any",
Value: Movie{
Name: "Star Wars",
Views: 10,
},
Duration: time.Minute,
},
}
err := cache.PutMany(entries...)
// handle err
To increment and decrement values (for now you can only increment & decrement using int64
values) simply use Increment
& Decrement
. Please note that if there is no entry for the key being incremented the initial value will be 0 plus whatever value was passed in and the entry will be set to not expire:
val, err := cache.Increment("a", 1) // a = 1
// handle err
val, err := cache.Increment("a", 10) // a = 11
// handle err
val, err := cache.Decrement("a", 2) // a = 9
// handle err
val, err := cache.Decrement("b", 5) // b = -5
// handle err
You may remove items from the cache using the Forget
or ForgetMany
methods:
// Note that res will be true if the cache entry was removed and false
// if no entry was for the given key
res, err := cache.Forget("key")
// handle err
// If no error is returned you can assume all keys where deleted. Not present keys will be
// ignored
err := cache.ForgetMany("key1", "key2", "key3")
// handle err
If you want to clear all entries from the cache you can use the Flush
method:
err := cache.Flush()
// handle err
Cache tags allow you to tag related items in the cache and then flush all cached values that have been assigned a given tag. You may access a tagged cache by passing in an ordered sliced of tag names. For example, let's access a tagged cache and put a value into the cache:
err := cache.Tags("person", "artist").Put("John", "Doe", time.Minute)
// handle err
err := cache.Tags("person", "accountant").Put("Jane", "Doe", time.Minute)
// handle err
To retrieve a tagged cache item, pass the same ordered list of tags to the Tags
method and then call the any of the methods shown in the Retrieving Items From The Cache section above:
v, err := cache.Tags("person", "artist").GetString("John")
// handle err
v, err := cache.Tags("person", "accountant").GetString("Jane")
// handle err
You may flush all items that are assigned a tag or list of tags. For example, this statement would remove all caches tagged with either person
, accountant
, or both. So, both Jane
and John
would be removed from the cache:
err := cache.Tags("person", "accountant").Flush()
// handle err
In contrast, this statement would remove only cached values tagged with accountant
, so Jane
would be removed, but not John
:
err := cache.Tags("accountant").Flush()
// handle err
In addition you can also call Forget
:
res, err := cache.Tags("person", "accountant").Forget("Jane")
// handle err
Important Note: with the exception of the Redis driver, when calling Flush
with tags, the underlying entries won't be deleted so please make sure to set expiration values when using tags and flushing.
In order to interact with tags directly you can call TagSet
on a tagged cache:
// Obtain the underlying tagged cache TagSet instance
ts := cache.Tags("person", "accountant").TagSet()
// Hard deletes cache entries used to hold tag set tags
err := ts.Flush()
// handle err
// Returns the tag ids associated with the provided tag set tags
ids, err := ts.TagIds()
// handle err
// Resets all the tag set tags
err := ts.Reset()
Atomic locks allow for the manipulation of distributed locks without worrying about race conditions. An example of this would be that you only want one process to work on one object at a time, such as the same file should only be uploaded one at a time (we do not want the same file to be uploaded more than once at any given time). You may create and manage locks via the Lock
method:
var (
lock = cache.Lock("merchant_1", "pid_1", 30 * time.Second)
acquired, err = lock.Acquire()
)
if err != nil {
// handle err
}
if acquired {
defer func() {
// released will be true if the lock was released before expiration
released, err := lock.Release()
// handle err
}
// do something here
}
The lock Get
method accepts a closure. After the closure is executed, the lock will automatically be released:
acquired, err := cache.Lock("merchant_1", "pid_1", 30 * time.Second).Get(func() error {
// do something cool here
})
// handle err
if !acquired {
// do something
}
If the lock is not available at the moment you request it, you may instruct the lock to wait for a given duration using a specified wait interval y using the Block
method. If the lock cannot be acquired within the specified time limit, a gocache.ErrBlockWaitTimeout
will be raised:
var (
lock = cache.Lock("merchant_1", "pid_1", time.Minute)
acquired, err = lock.Block(time.Second, 30 * time.Second, func() error {
// do something cool here
})
)
// handle err
if !acquired {
// do something
}
This package includes a simple to use rate limiter implementation which, in conjunction with a gocache.Cache
instance, provides an easy way to limit any set of operations for a predetermined window of time.
The underlying logic of the Rate Limiter implementation allows for a max number of hits x
for a given duration y
. If x
is exceeded during the y
timeframe the Rate Limiter will limit further actions until duration y
expires. Once duration y
is expired the number of hits x
will reset back to 0.
The most basic usage of the Rate Limiter is to throttle a given action. To do so you can call the Throttle
method on a Rate Limiter instance as such:
var (
// new a RateLimiter instance
rateLimiter = gocache.NewRateLimiter(cache) // where cache is a gocache.Cache instance
maxAttempts = 100
window = 2 * time.Second
res, err = rateLimiter.Throttle("some-key", maxAttempts, window, func() error {
// Do something here
})
)
// handle err
// check if request is throttled
if !res.IsThrottled() {
// check for remaining attempts
remaining := res.RemainingAttempts()
// ...
} else {
// check how much you need to wait
wait := res.RetryAfter()
// ...
}
For more flexible functionality you can call the following methods individually:
// new a RateLimiter instance
rateLimiter := gocache.NewRateLimiter(cache) // where cache is a gocache.Cache instance
// increment the number of hits for a given key for a given time decay
hits, err := rateLimiter.Hit("some-key", time.Minute)
// handle err
// get the number of hits for a given key
hits, err = rateLimiter.Attempts("some-key")
// handle err
// get the number of hits left
remaining, err := rateLimiter.AttemptsLeft("some-key")
// handle err
// check if number of hits has been exceeded
exceeded, err := rateLimiter.TooManyAttempts("some-key", maxAttempts) // maxAttempts = 100
// handle err
// reset the number of attempts for a given key
err = rateLimiter.Clear("some-key")
// handle err
Find an area you can help with and do it. Open source is about collaboration and open participation. Try to make your code look like what already exists or hopefully better and submit a pull request. Also, if you have any ideas on how to make the code better or on improving its scope and functionality please raise an issue and I will do my best to address it in a timely manner.
MIT.