Understanding Go's singleflight: Request Merging, Implementation and Use Cases
singleflight, a Go concurrency primitive from the x/sync package, merges duplicate in‑flight requests to reduce server load, with detailed usage examples, source code analysis, and discussion of its differences from sync.Once and typical application scenarios such as cache‑penetration, remote calls, and task deduplication.
singleflight is a concurrency primitive provided by the golang.org/x/sync package that suppresses duplicate in‑flight calls, allowing multiple goroutines requesting the same resource to share a single execution result and thus reduce server pressure.
Request Merging
The primitive is primarily used to suppress repeated concurrent calls . When several goroutines invoke the same function simultaneously, only one goroutine actually runs the function while the others block and receive the same result once the first call completes. This is especially useful in high‑concurrency scenarios such as cache‑miss bursts that would otherwise flood a database.
SingleFlight Usage Example
package main
import (
"fmt"
"strconv"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
var (
cache = make(map[string]*User) // simulated cache
mu sync.RWMutex // protects cache
requestGroup singleflight.Group // SingleFlight instance
)
type User struct {
Id int64
Name string
Email string
}
func GetUserFromDB(username string) *User {
fmt.Printf("Querying DB for key: %s\n", username)
time.Sleep(1 * time.Second) // simulate latency
id, _ := strconv.Atoi(username[len(username)-3:])
return &User{Id: int64(id), Name: username, Email: username + "@example.com"}
}
func GetUser(key string) *User {
mu.RLock()
val, ok := cache[key]
mu.RUnlock()
if ok {
return val
}
fmt.Printf("User %s not in cache\n", key)
result, _, _ := requestGroup.Do(key, func() (interface{}, error) {
user := GetUserFromDB(key)
mu.Lock()
cache[key] = user
mu.Unlock()
return user, nil
})
return result.(*User)
}
func main() {
var wg sync.WaitGroup
keys := []string{"user_123", "user_123", "user_456"}
// First round – cache empty, requests are merged
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
fmt.Printf("Get user for key: %s -> %+v\n", k, GetUser(k))
}(key)
}
time.Sleep(2 * time.Second)
fmt.Println("===================================")
// Second round – cache populated, no DB calls
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
fmt.Printf("Get user for key: %s -> %+v\n", k, GetUser(k))
}(key)
}
wg.Wait()
}The example demonstrates a typical cache‑miss scenario. The first round triggers a DB query only once for user_123 because the two goroutines requesting the same key are merged by singleflight . After the results are cached, the second round reads directly from the cache without any DB access.
SingleFlight API
Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) – executes fn if the key is not already in flight; otherwise waits for the existing call and returns the shared result.
DoChan(key string, fn func() (interface{}, error)) <-chan Result – similar to Do but returns a channel that receives the result, allowing non‑blocking callers.
Forget(key string) – removes the key from the internal map, forcing subsequent calls to treat it as a fresh request.
Do Implementation
The method locks the group, checks whether a call for the given key already exists, and either increments the duplicate counter and waits on the existing call 's WaitGroup , or creates a new call , stores it, and invokes doCall to run the user function.
DoChan Implementation
It creates a buffered channel, registers it in the call 's chans slice if the key is already in flight, or creates a new call with the channel and launches doCall in a new goroutine. Results are later broadcast to all registered channels.
doCall Core Logic
The function runs the user fn inside a nested anonymous function to capture panics via recover . It records whether the function returned normally ( normalReturn ) or panicked ( recovered ). After execution it unlocks the group, marks the WaitGroup as done, removes the key from the map, and:
If a panic occurred, it wraps the panic value in a panicError (including a stack trace) and either re‑panics directly (for Do ) or launches a separate goroutine that panics and blocks forever (for DoChan ) to avoid dead‑locking waiting receivers.
If the function called runtime.Goexit , the sentinel error errGoexit is stored.
On normal return, the result is sent to all channels in call.chans (if any) and stored in call.val and call.err for callers of Do .
Forget
Simply deletes the key from the internal map, useful for abandoning long‑running calls, cleaning up erroneous state, or forcing a retry.
Typical Application Scenarios
Cache Penetration – when a hot key expires, singleflight ensures only one request hits the database while the rest wait for the result.
Remote Service Calls – duplicate RPCs for the same request are merged, saving bandwidth and latency.
Scheduled Task De‑duplication – in distributed systems only one node performs the job, others share the outcome.
Message De‑duplication – consumers process a message once even if it appears multiple times.
Distributed Lock Optimization – reduces contention by allowing only a single lock‑acquire attempt for a given key.
singleflight vs. sync.Once
singleflight works only while there are concurrent duplicate requests; after the call finishes the key is removed, so subsequent calls will execute the function again. In contrast, sync.Once guarantees that a function is executed at most once for the lifetime of the Once value, regardless of concurrency.
Conclusion
singleflight is a powerful tool for suppressing redundant concurrent operations, improving performance in high‑traffic services. Its three public methods ( Do , DoChan , Forget ) provide flexible ways to merge calls, obtain results asynchronously, and manage the internal state. Understanding its implementation helps developers apply it correctly and differentiate it from other synchronization primitives such as sync.Once .
Go Programming World
Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.