This gofnext provides the following functions extended(go>=1.21).
Cache decorators(concurrent safe): Similar to Python's functools.cache
and functools.lru_cache
.
In addition to memory caching, it also supports Redis caching and custom caching.
function | decorator |
---|---|
func f() res | gofnext.CacheFn0(f) |
func f(a) res | gofnext.CacheFn1(f) |
func f(a,b) res | gofnext.CacheFn2(f) |
func f() (res,err) | gofnext.CacheFn0Err(f) |
func f(a) (res,err) | gofnext.CacheFn1Err(f) |
func f(a,b) (res,err) | gofnext.CacheFn2Err(f) |
func f() (res,err) | gofnext.CacheFn0Err(f, &gofnext.Config{TTL: time.Hour}) // memory cache with ttl |
func f() (res) | gofnext.CacheFn0(f, &gofnext.Config{CacheMap: gofnext.NewCacheLru(9999)}) // Maxsize of cache is 9999 |
func f() (res) | gofnext.CacheFn0(f, &gofnext.Config{CacheMap: gofnext.NewCacheRedis("cacheKey")}) // Warning: redis's marshaling may result in data loss |
- Cache Decorator (gofnext)
- Decorator cache for function
- Concurrent goroutine Safe
- Support memory CacheMap(default)
- Support memory-lru CacheMap
- Support redis CacheMap
- Support customization of the CacheMap(manually)
Refer to: examples
Refer to: decorator fib example
package main
import "fmt"
import "github.com/ahuigo/gofnext"
func main() {
var fib func(int) int
fib = func(x int) int {
fmt.Printf("call arg:%d\n", x)
if x <= 1 {
return x
} else {
return fib(x-1) + fib(x-2)
}
}
fib = gofnext.CacheFn1(fib)
fmt.Println(fib(5))
fmt.Println(fib(6))
}
Refer to: decorator example
package examples
import "github.com/ahuigo/gofnext"
func getUserAnonymouse() (UserInfo, error) {
fmt.Println("select * from db limit 1", time.Now())
time.Sleep(10 * time.Millisecond)
return UserInfo{Name: "Anonymous", Age: 9}, errors.New("db error")
}
var (
// Cacheable Function
getUserInfoFromDbWithCache = gofnext.CacheFn0Err(getUserAnonymouse)
)
func TestCacheFuncWithNoParam(t *testing.T) {
// Execute the function multi times in parallel.
times := 10
parallelCall(func() {
userinfo, err := getUserInfoFromDbWithCache()
fmt.Println(userinfo, err)
}, times)
}
Refer to: decorator example
func getUserNoError(age int) (UserInfo) {
time.Sleep(10 * time.Millisecond)
return UserInfo{Name: "Alex", Age: age}
}
var (
// Cacheable Function with 1 param and no error
getUserInfoFromDbNil= gofnext.CacheFn1(getUserNoError)
)
func TestCacheFuncNil(t *testing.T) {
// Execute the function multi times in parallel.
times := 10
parallelCall(func() {
userinfo := getUserInfoFromDbNil(20)
fmt.Println(userinfo)
}, times)
}
Refer to: decorator example
func TestCacheFuncWith2Param(t *testing.T) {
// Original function
executeCount := 0
getUserScore := func(c context.Context, id int) (int, error) {
executeCount++
fmt.Println("select score from db where id=", id, time.Now())
time.Sleep(10 * time.Millisecond)
return 98 + id, errors.New("db error")
}
// Cacheable Function
getUserScoreFromDbWithCache := gofnext.CacheFn2Err(getUserScore, &gofnext.Config{
TTL: time.Hour,
}) // getFunc can only accept 2 parameter
// Execute the function multi times in parallel.
ctx := context.Background()
parallelCall(func() {
score, _ := getUserScoreFromDbWithCache(ctx, 1)
if score != 99 {
t.Errorf("score should be 99, but get %d", score)
}
getUserScoreFromDbWithCache(ctx, 2)
getUserScoreFromDbWithCache(ctx, 3)
}, 10)
if executeCount != 3 {
t.Errorf("executeCount should be 3, but get %d", executeCount)
}
}
Refer to: decorator example
executeCount := 0
type Stu struct {
name string
age int
gender int
}
// Original function
fn := func(name string, age, gender int) int {
executeCount++
// select score from db where name=name and age=age and gender=gender
switch name {
case "Alex":
return 10
default:
return 30
}
}
// Convert to extra parameters to a single parameter(2 prameters is ok)
fnWrap := func(arg Stu) int {
return fn(arg.name, arg.age, arg.gender)
}
// Cacheable Function
fnCachedInner := gofnext.CacheFn1(fnWrap)
fnCached := func(name string, age, gender int) int {
return fnCachedInner(Stu{name, age, gender})
}
// Execute the function multi times in parallel.
parallelCall(func() {
score := fnCached("Alex", 20, 1)
if score != 10 {
t.Errorf("score should be 10, but get %d", score)
}
fnCached("Jhon", 21, 0)
fnCached("Alex", 20, 1)
}, 10)
// Test Count
if executeCount != 2 {
t.Errorf("executeCount should be 2, but get %d", executeCount)
}
Refer to: decorator lru example
executeCount := 0
maxCacheSize := 2
var getUserScore = func(more int) (int, error) {
executeCount++
return 98 + more, errors.New("db error")
}
// Cacheable Function
var getUserScoreFromDbWithLruCache = gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
TTL: time.Hour,
CacheMap: gofnext.NewCacheLru(maxCacheSize),
})
Warning: Since redis needs JSON marshaling, this may result in data loss.
Refer to: decorator redis example
var (
// Cacheable Function
getUserScoreFromDbWithCache = gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
TTL: time.Hour,
CacheMap: gofnext.NewCacheRedis("redis-cache-key"),
})
)
func TestRedisCacheFuncWithTTL(t *testing.T) {
// Execute the function multi times in parallel.
for i := 0; i < 10; i++ {
score, _ := getUserScoreFromDbWithCache(1)
if score != 99 {
t.Errorf("score should be 99, but get %d", score)
}
}
}
To avoid keys being too long, you can limit the length of Redis key:
cacheMap := gofnext.NewCacheRedis("redis-cache-key").SetMaxHashKeyLen(256);
Set redis config:
// method 1: by default: localhost:6379
cache := gofnext.NewCacheRedis("redis-cache-key")
// method 2: set redis addr
cache.SetRedisAddr("192.168.1.1:6379")
// method 3: set redis options
cache.SetRedisOpts(&redis.Options{
Addr: "localhost:6379",
})
// method 4: set redis universal options
cache.SetRedisUniversalOpts(&redis.UniversalOptions{
Addrs: []string{"localhost:6379"},
})
Refer to: https://github.com/ahuigo/gofnext/blob/main/cache-map-mem.go
gofnext.Config item list:
Key | Description | Default |
---|---|---|
TTL | Cache Time to Live | 0(No timeout) |
CacheMap | Custom own cache | Inner Memory |
NeedCacheIfErr | Enable cache even if there is an error | false |
HashKeyPointerAddr | Use Pointer Addr(&p) as key instead of its value when hashing key | false(Use real value*p as key) |
HashKeyFunc | Custom hash key function | Inner hash func |
e.g.
gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
TTL: time.Hour,
})
By default, gofnext won't cache error when there is an error.
To use the cache even when there is an error, simply add NeedCacheIfErr: true
.
Refer to: https://github.com/ahuigo/gofnext/blob/main/examples/decorator-err_test.go
gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
NeedCacheIfErr: true,
})
Decorator will hash function's all parameters into hashkey. By default, if parameter is pointer, decorator will hash its real value instead of pointer address.
If you wanna hash pointer address, you should turn on HashKeyPointerAddr
:
getUserScoreFromDbWithCache := gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
HashKeyPointerAddr: true,
})
In this case, you need to ensure that duplicate keys are not generated. Refer to: example
// hash key function
hashKeyFunc := func(keys ...any) []byte{
user := keys[0].(*UserInfo)
flag := keys[1].(bool)
return []byte(fmt.Sprintf("user:%d,flag:%t", user.id, flag))
}
// Cacheable Function
getUserScoreFromDbWithCache := gofnext.CacheFn2Err(getUserScore, &gofnext.Config{
HashKeyFunc: hashKeyFunc,
})
- [] Include private property when serialization for redis