Golang Go语言中利用泛型实现的缓存装饰器

项目地址: https://github.com/ahuigo/gofnext#decorator-cases

类似于 python 的 @functools.cache

cache decorator 菲波那切数列示例:

package main
import "fmt"
import "github.com/ahuigo/gofnext"
func main() {
    var fib func(int) int
    var fibCached func(int) int
    fib = func(x int) int {
        fmt.Printf("call arg:%d\n", x)
        if x <= 1 {
            return x
        } else {
            return fibCached(x-1) + fibCached(x-2)
        }
    }
fibCached = gofnext.CacheFn1(fib)    

fmt.Println(fibCached(5))
fmt.Println(fibCached(6))

}


Golang Go语言中利用泛型实现的缓存装饰器

更多关于Golang Go语言中利用泛型实现的缓存装饰器的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

看上去不够优雅,但是如果要适配任意数目的参数和返回值,只能用反射来做。

更多关于Golang Go语言中利用泛型实现的缓存装饰器的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


多参数的话不需要用反射。
其实以前我用反射实现过,使用起来更复杂、且会损失类型检查,目前用泛型实现就简单许多了。

如果是多参数的话,需要用包装函数(不需要反射)将参数将降维(示例: https://github.com/ahuigo/gofnext#cache-function-with-more-params2

ps: 之所以没有封装成支持任意不定参或 any 类型的函数,是因为目前 go 的泛型参数不支持对约束再做断言。

如果你有更优雅方式的话,望回复告之,也欢迎 issue 、pr 。

反射不会损失类型检查,可以动态获取参数类型并构造一个 struct ,但是做不到静态检查。实现自然是会复杂很多,但是对调用者而言是更简单的。不过既然你是做缓存,肯定是在意性能的,我就不推荐用反射了。

如果输入用反射而不用泛型,那类型检查就没有了; 输出同理。

或许我没有 get 到你的意思?你是否可以给个伪代码,或者 demo ?

ps: 反射损失的性能很多场景可以忽略不计

我的意思是反射可以获取到原函数的每个参数的类型,你可以保存下来,调用时检查参数是不是对应的类型。

但是 Go 不是很动态的语言,反射和泛型也没法结合使用,导致泛型实现的接口没法返回正确的类型(只能是 interface{}),因此没法实现 demo 的 fibCached(x-1) + fibCached(x-2)。不过 demo 里对 fib() 的实现也是有侵入的。

比较类似的例子你可以参考这篇,最后为了优化实现得有点复杂,看看原理就好:
https://keakon.uk/2023/03/24/%E6%8A%8A%E4%BB%BB%E5%8A%A1%E9%98%9F%E5%88%97delayed%E7%A7%BB%E6%A4%8D%E5%88%B0Go%E4%BA%86

嗯。
我主要是想确认否有简单优雅的实现(对调用方要简单/直接/安全)、以及更通用的 cache 库。

库本身的复杂性对用户是透明的,内部是否反射+泛型都 ok ,实际上本库的内部就是反射+泛型结合的

在Go语言中,泛型是Go 1.18引入的一项强大特性,它允许我们编写更加通用和可复用的代码。利用泛型来实现缓存装饰器(Cache Decorator)是一个很好的实践,这不仅能提高代码的灵活性,还能增强代码的可维护性。

一个缓存装饰器的核心思想是拦截对某个底层数据结构的访问,首先检查缓存中是否已存在所需数据,如果存在则直接返回缓存中的数据,以避免重复计算和访问底层数据结构带来的开销。如果不存在,则访问底层数据结构获取数据,并将其存入缓存中供后续使用。

在实现时,你可以定义一个泛型接口来表示底层数据结构,例如interface Cacheable<T>,然后实现一个泛型缓存装饰器Cached<T>。这个装饰器内部可以维护一个map作为缓存存储,key是访问底层数据结构所需的参数,value是返回的数据。

通过使用泛型,你的缓存装饰器可以适用于各种不同类型的底层数据结构,而无需为每种类型编写专门的缓存逻辑。这不仅减少了代码量,还提高了代码的复用性和可读性。

在实现过程中,还需要注意缓存的失效策略(如LRU、TTL等)和线程安全问题,以确保缓存的有效性和正确性。对于线程安全问题,可以利用Go的sync包提供的并发原语(如sync.Mutex、sync.RWMutex)来保护对缓存的并发访问。

总之,利用泛型实现缓存装饰器是Go语言中一种高效且优雅的编程方式,值得在实际开发中推广和应用。

回到顶部