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 |
Benchmark Benchmark case: https://github.com/ahuigo/gofnext/blob/main/bench/
# golang1.22
pkg: github.com/ahuigo/gofnext/bench
BenchmarkGetDataWithNoCache-10 100 11285146 ns/op 281286 B/op 99 allocs/op
BenchmarkGetDataWithMemCache-10 13926818 86.33 ns/op 72 B/op 2 allocs/op
BenchmarkGetDataWithLruCache-10 12431094 95.57 ns/op 72 B/op 2 allocs/op
BenchmarkGetDataWithRedisCache-10 15058 77713 ns/op 28020 B/op 24 allocs/op
- Cache Decorator (gofnext)
- Decorator cache for function
- Concurrent goroutine Safe
- Support memory CacheMap(default)
- Support memory-lru CacheMap
- Support redis CacheMap
- Support postgres 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
- Postgres cache extension: https://github.com/ahuigo/gofnext_pg
gofnext.Config item list:
Key | Description | Default |
---|---|---|
TTL | Cache Time to Live | 0(0:Permanent cache error; >0:Cache with TTL) |
ErrTTL | cache TTL for error return if there is an error | 0(0:Donot cache error; >0:Cache error with TTL; -1:rely on TTL only; ) |
CacheMap | Custom own cache | Inner Memory |
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.
If there is an error, and you wanna control the error cache's TTL, simply add ErrTTL: time.Duration
.
Refer to: https://github.com/ahuigo/gofnext/blob/main/examples/decorator-err_test.go
gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
ErrTTL: 0, // Do not cache error
ErrTTL: time.Seconds * 60, // error cache's errTTL is 60s
ErrTTL: -1, // rely on TTL only
})
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