go / singleflight
I use singleflight to deduplicate concurrent function calls. When multiple goroutines request the same thing simultaneously, only one does the work and the others wait for its result.
The problem
Without deduplication, a cache miss can cause a thundering herd:
func GetUser(id string) (*User, error) {
if user, ok := cache.Get(id); ok {
return user, nil
}
// 100 concurrent requests all miss the cache
// and hit the database simultaneously
return db.QueryUser(id)
}
Generic singleflight
type call[V any] struct {
wg sync.WaitGroup
val V
err error
}
type Group[K comparable, V any] struct {
mu sync.Mutex
m map[K]*call[V]
}
func (g *Group[K, V]) Do(key K, fn func() (V, error)) (V, error) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[K]*call[V])
}
// If call in progress, wait for it
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err
}
// Start new call
c := &call[V]{}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
c.val, c.err = fn()
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
c.wg.Done()
return c.val, c.err
}
Usage
var userGroup Group[string, *User]
func GetUser(id string) (*User, error) {
if user, ok := cache.Get(id); ok {
return user, nil
}
// Only one goroutine queries the database
// Others wait and receive the same result
user, err := userGroup.Do(id, func() (*User, error) {
return db.QueryUser(id)
})
if err != nil {
return nil, err
}
cache.Set(id, user)
return user, nil
}
When to use
- Cache population on miss
- Expensive computations with identical inputs
- External API calls with rate limits
- Any idempotent operation called concurrently with the same arguments
Standard library
Go's golang.org/x/sync/singleflight provides this,
but without generics. The generic version avoids type assertions:
// Without generics
val, err, _ := group.Do(key, func() (any, error) { ... })
user := val.(*User) // type assertion required
// With generics
user, err := group.Do(key, func() (*User, error) { ... })