Skip to content

Latest commit

 

History

History
372 lines (306 loc) · 13.1 KB

readme.md

File metadata and controls

372 lines (306 loc) · 13.1 KB

🛠️ 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() R gofnext.CacheFn0(f)
func f(K1) R gofnext.CacheFn1(f)
func f(K1,K2) R gofnext.CacheFn2(f)
func f(K1,K2,K3) R gofnext.CacheFn3(f)
func f() (R,error) gofnext.CacheFn0Err(f)
func f(K1) (R,error) gofnext.CacheFn1Err(f)
func f(K1, K2) (R,error) gofnext.CacheFn2Err(f)
func f() (R,error) gofnext.CacheFn0Err(f, &gofnext.Config{TTL: time.Hour})
// memory cache with ttl
func f() R gofnext.CacheFn0(f, &gofnext.Config{CacheMap: gofnext.NewCacheLru(9999)})
// Maxsize of cache is 9999
func f() R 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          11179015 ns/op          281220 B/op         99 allocs/op
BenchmarkGetDataWithMemCache-10         11036955                95.49 ns/op           72 B/op          2 allocs/op
BenchmarkGetDataWithLruCache-10         11362039               104.8 ns/op            72 B/op          2 allocs/op
BenchmarkGetDataWithRedisCache-10          15850             74653 ns/op           28072 B/op         29 allocs/op

Features

  • 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)
  • Common functions
    • I recommend gox, it provides ternary operator(If/IfFunc),Ptr,Pie,....

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 or 3 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
    getUserScoreWithCache := 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, _ := getUserScoreWithCache(ctx, 1)
        if score != 99 {
            t.Errorf("score should be 99, but get %d", score)
        }
        getUserScoreWithCache(ctx, 2)
        getUserScoreWithCache(ctx, 3)
    }, 10)

    if executeCount != 3 {
        t.Errorf("executeCount should be 3, but get %d", executeCount)
    }
}

Cache function with more params(>3)

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
    getUserScoreWithCache = 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, _ := getUserScoreWithCache(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

Extension(pg)

Decorator config

Config item(gofnext.Config)

gofnext.Config item list:

Key Description Default
TTL Cache Time to Live 0(if TTL=0:use permanent cache; if TTL>0:set 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

Cache's Live Time(TTL)

For example: set cache's live time to 1hour.

gofnext.CacheFn1Err(getUserScore, &gofnext.Config{
    /* Set cache's TTL time: 
        if TTL==0, use permanent cache; 
        if TTL>0, cache's live time is TTL
    */
    TTL:  time.Hour, 
}) 

Error Cache's Live Time(ErrTTl)

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{
    /* Set error cache's TTL time:
        if ErrTTL=0, do not cache error;
        if ErrTTL>0, error cache's live time is ErrTTL;
        if ErrTTL=-1, error cache's live time is controlled by TTL
    */
    ErrTTL: 0, // Do not cache error(default:0)
    ErrTTL: time.Seconds * 60, // error cache's live time is 60s
    ErrTTL: -1, // rely on TTL only
}) 

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:

getUserScoreWithCache := 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
getUserScoreWithCache := gofnext.CacheFn2Err(getUserScore, &gofnext.Config{
	HashKeyFunc: hashKeyFunc,
})

Roadmap

  • [] Include private property when serializating for redis(#spec/reflect/unexported)