Problems this pattern can solve:
- The "Thundering Herd" problem. Imagine: a cache with user data has expired. 100 concurrent requests arrive, all see the cache miss and simultaneously stampede to the database. The database collapses under load, the service dies.
- A microservice calls an external API with strict rate limits. Under high load, 10 parallel requests with the same key could exhaust the limit or simply create redundant traffic. (All requests with the same key are coalesced into a single external call, saving limits and resources.)
- Heavy operations: web page parsing via a headless browser, generating PDF reports, ML inference. If 50 users request the same report, launching 50 browsers is memory suicide. (One browser will be launched, the result will be shared by all.)
Essence: Singleflight guarantees that for a given key, only one operation is executed at a time. All other calls with the same key do not start a new operation but "attach" themselves to the already running one and wait for its result. After the original call completes, all waiters receive the same result (or error).
Idea: Coalesce multiple concurrent requests with the same key into a single real call and distribute the result to all waiters.
Use the official package golang.org/x/sync/singleflight.
Singleflight vs Other Patterns
Difference from Caching.
Cache: Stores the result after execution. On concurrent access to an empty cache, all requests will still go to the DB (cache miss).
Singleflight: Doesn't store the result, but coalesces requests at the moment. After completion, the result is not saved (unless you put it in the cache yourself).
Difference from Semaphore (worker pool).
Semaphore: Limits the number of concurrently executing operations, but doesn't coalesce identical ones. 10 requests for the same key can execute in parallel (if the pool allows).
Singleflight: Regardless of pool size, only one operation is ever executed for a given key.
Difference from WaitGroup.
WaitGroup: Simply waits for a group of goroutines to complete, but doesn't manage duplicates or share results.
Singleflight: Uses WaitGroup internally to coordinate waiting goroutines and distribute the result.
Difference from Mutex.
Mutex: Locks access to a critical section, but each time the mutex is released, a new thread enters and repeats the operation.
Singleflight: The operation is executed once, the result is given to all.
Example
package main
import (
"fmt"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
func main() {
var group singleflight.Group
var wg sync.WaitGroup
key := "user:123"
requests := 5
// Simulating a heavy operation (database call or external API)
expensiveOp := func() (interface{}, error) {
fmt.Println("Heavy operation started (actually once)")
time.Sleep(2 * time.Second) // simulating long work
fmt.Println("Heavy operation completed")
return "data_for_" + key, nil
}
// Launching 5 concurrent requests
for i := 0; i < requests; i++ {
wg.Add(1)
go func(reqID int) {
defer wg.Done()
// Execute via singleflight
result, err, shared := group.Do(key, expensiveOp)
if err != nil {
fmt.Printf("Request %d: error %v\n", reqID, err)
return
}
fmt.Printf("Request %d: result = %v (shared = %v)\n",
reqID, result, shared)
}(i)
}
wg.Wait()
}

Top comments (0)