Design a thread-safe generic cache with a GetOrLoad method. What concurrency pitfalls exist?
Question 135HardGo 1.22 to 1.25
type Cache[K comparable, V any] struct {
mu sync.Mutex
items map[K]*entry[V]
}
type entry[V any] struct {
once sync.Once
val V
err error
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{items: make(map[K]*entry[V])}
}
// GetOrLoad calls load at most once per key, even under concurrency,
// and never holds the global lock while load runs.
func (c *Cache[K, V]) GetOrLoad(k K, load func(K) (V, error)) (V, error) {
c.mu.Lock()
e, ok := c.items[k]
if !ok {
e = &entry[V]{}
c.items[k] = e
}
c.mu.Unlock()
e.once.Do(func() { e.val, e.err = load(k) })
if e.err != nil {
c.mu.Lock()
if c.items[k] == e { // do not cache failures
delete(c.items, k)
}
c.mu.Unlock()
}
return e.val, e.err
}
Pitfalls to discuss:
- Holding the mutex during
loadserializes all keys. Per-keysync.Onceavoids that and deduplicates concurrent loads for the same key, likesingleflight. - Decide whether errors are cached. The example removes failed entries.
- The map grows without bound. Add an LRU list or a TTL.
- If
Vis a pointer or slice, callers can mutate cached data. sync.Mapis not generic. Wrapping it means paying for type assertions, whereas this version is type-safe.
More on Generics
- Q133Implement a generic binary search tree keyed by
cmp.Orderedwith an in-order iterator. - Q134Implement a generic
Set[T]with union, intersection and iteration. - Q136Write generic
Map,FilterandReducefor slices. Why aren't they in the standard library'sslicespackage? - Q137What is
golang.org/x/exp/constraints, how does it relate tocmp.Ordered, and how do you define your own numeric constraint? - Q138Explain the
cmppackage:Compare,Less,Or. How do you do a multi-key sort, and how is NaN handled? - Q139Which generic helpers in
slicesandmapsshould a senior Go developer know, and what are their gotchas?