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

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) { ... })

← All articles