What problem does singleflight solve, and what are its gotchas?
Question 247HardGo 1.22 to 1.25
golang.org/x/sync/singleflight deduplicates concurrent calls for the same key. While one call for a key is in flight, later callers wait for it and share its result. This prevents a cache stampede (thundering herd): when a hot cache entry expires, thousands of requests would otherwise hit the database at the same moment.
type UserService struct {
sf singleflight.Group
cache *Cache
db *DB
}
func (s *UserService) Get(ctx context.Context, id string) (*User, error) {
if u, ok := s.cache.Get(id); ok {
return u, nil
}
v, err, _ := s.sf.Do(id, func() (any, error) {
// detach from the first caller's cancellation, but keep a bound
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
defer cancel()
u, err := s.db.LoadUser(ctx, id)
if err == nil {
s.cache.Set(id, u)
}
return u, err
})
if err != nil {
return nil, err
}
return v.(*User), nil
}
Gotchas:
- The function runs with the first caller's context. If that caller cancels, every waiter fails. Use
WithoutCancelplus a timeout, as above. Doignores each waiter's own context. UseDoChanwith aselectso each caller can give up independently.- The result is shared, so treat it as immutable.
- Errors are shared too.
Forget(key)lets the next caller start a new call. - It only deduplicates within one process.
More on Concurrency Patterns & sync
- Q245You need to process 10 million records from a file with bounded memory and bounded parallelism. How do you design it?
- Q246How do you implement rate limiting in Go? Compare time.Ticker with golang.org/x/time/rate.
- Q248Implement graceful shutdown for an HTTP server with background workers.
- Q249Which newer context features matter for concurrent code: WithCancelCause, AfterFunc, WithoutCancel?
- Q250Is time.After in a select loop a leak? What changed in Go 1.23 timers?
- Q251What happens here, and when does the runtime NOT detect a deadlock?