【问题标题】:Add a cache to a go function as if it were a static member将缓存添加到 go 函数,就好像它是静态成员一样
【发布时间】:2021-06-01 11:47:54
【问题描述】:

假设我有一个昂贵的功能

func veryExpensiveFunction(int) int

这个函数会被同一个号码调用很多次。

是否有一种好方法可以让该函数存储以前的结果以在再次调用该函数时使用,甚至可能对veryExpensiveFunction2 可重用?

显然,可以添加一个参数

func veryExpensiveFunctionCached(p int, cache map[int]int) int {
    if val, ok := cache[p]; ok {
        return val
    }
    result := veryExpensiveFunction(p)
    cache[p] = result
    return result
}

但是现在我必须在某个我不关心它的地方创建缓存。如果可能的话,我宁愿将它作为“静态函数成员”。

在go中模拟静态成员缓存的好方法是什么?

【问题讨论】:

  • 只需使用包级映射或类似的。
  • @Beginner 请检查我的答案,因为它还处理并发竞争条件等。
  • @Peter 那将是一个包全局变量,对吧?我更喜欢在我的函数中使用本地的东西。我如何为此证明一个全局变量?
  • @EduardHasanaj 是的,我投了赞成票。我正在测试实现。为什么这么着急?
  • 在计算机科学中这称为Memoization。使用这个词进行搜索应该会带来有趣的结果。

标签: go memoization


【解决方案1】:

你可以使用闭包;并让闭包管理缓存。

func InitExpensiveFuncWithCache() func(p int) int {
    var cache = make(map[int]int)
    return func(p int) int {
        if ret, ok := cache[p]; ok {
            fmt.Println("from cache")
            return ret
        }
        // expensive computation
        time.Sleep(1 * time.Second)
        r := p * 2
        cache[p] = r
        return r
    }
}

func main() {
    ExpensiveFuncWithCache := InitExpensiveFuncWithCache()
    
    fmt.Println(ExpensiveFuncWithCache(2))
    fmt.Println(ExpensiveFuncWithCache(2))
}

output:
4
from cache
4

veryExpensiveFunctionCached := InitExpensiveFuncWithCache()

并在您的代码中使用包装函数。 你可以试试here

如果您希望它可重用,请将签名更改为InitExpensiveFuncWithCache(func(int) int),以便它接受一个函数作为参数。将其包裹在闭包中,用它替换昂贵的计算部分。

【讨论】:

  • 如果像这里的初始化很简单,你可以使用func init()来初始化东西,所以不需要显式调用(从main等)
  • 我认为您的链接有一个杂散字符 - 但这里是:golang.org/doc/effective_go#init
  • 这真的很容易实现,而且效果很好,谢谢。
  • 如果不同的 goroutine 同时调用 ExpensiveFuncWithCache 会怎样?它是线程安全的吗?谢谢
【解决方案2】:

如果此缓存将在 http 处理程序中使用,您需要小心同步。在 Go 标准库中,每个 http 请求都在一个专用的 goroutine 中处理,此时我们处于并发和竞争条件的领域。我建议使用RWMutex 以确保数据一致性。

至于缓存注入,您可以在创建 http 处理程序的函数中注入它。 这是一个原型

type Cache struct {
    store map[int]int
    mux   sync.RWMutex
}

func NewCache() *Cache {
    return &Cache{make(map[int]int), sync.RWMutex{}}
}

func (c *Cache) Set(id, value int) {
    c.mux.Lock()
    c.store[id] = id
    c.mux.Unlock()
}

func (c *Cache) Get(id int) (int, error) {
    c.mux.RLock()
    v, ok := c.store[id]
    c.mux.RUnlock()

    if !ok {
        return -1, errors.New("a value with given key not found")
    }

    return v, nil
}


func handleComplexOperation(c *Cache) http.HandlerFunc {
    return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request){
        
    })
}

【讨论】:

    【解决方案3】:

    Go 标准库使用以下样式来提供“静态”函数(例如 flag.CommandLine),但会利用底层状态:

    // "static" function is just a wrapper
    func Lookup(p int) int { return expCache.Lookup(p) }
    
    var expCache = NewCache()
    
    func newCache() *CacheExpensive { return &CacheExpensive{cache: make(map[int]int)} }
    
    type CacheExpensive struct {
        l     sync.RWMutex // lock for concurrent access
        cache map[int]int
    }
    
    func (c *CacheExpensive) Lookup(p int) int { /*...*/ }
    

    这种设计模式不仅允许简单的一次性使用,还允许隔离使用:

    var (
        userX = NewCache()
        userY = NewCache()
    )
    
    userX.Lookup(12)
    userY.Lookup(42)
    

    【讨论】:

    • 而我昂贵的函数调用会发生在查找中,对吧?
    • 正确。如果您期望并发使用,则需要通过利用锁来保护缓存映射。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-02-24
    • 2019-10-19
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    相关资源
    最近更新 更多