Skip to content

ahuigo/gofnext

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

57 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ› ๏ธ Go function extended

tag Go Version GoDoc Build Status Go report Coverage Contributors License

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.

็ฎ€ไฝ“ไธญๆ–‡

Decorator cases

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

Features

  • 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)

Decorator examples

Refer to: examples

Cache fibonacii function

Refer to: decorator fib example

Play: https://go.dev/play/p/7BCINKENJzA

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))
}

Cache function with 0 param

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)
}

Cache function with 1 param

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)
}

Cache function with 2 params

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)
    }
}

Cache function with more params(>2)

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)
}

Cache function with lru cache

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),
})

Cache function with redis cache(unstable)

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"},
})

Custom cache map

Refer to: https://github.com/ahuigo/gofnext/blob/main/cache-map-mem.go

Decorator config

Config item(gofnext.Config)

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

Cache Timeout

e.g.

gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
    TTL:  time.Hour,
}) 

Do not cache if there is an error

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,
}) 

Hash Pointer address or value?

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,
})

Custom hash key function

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,
})

Roadmap

  • [] Include private property when serialization for redis