Backend Development 26 min read

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.

Go Programming World
Go Programming World
Go Programming World
Understanding Go's singleflight: Request Merging, Implementation and Use Cases

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 .

Cacheconcurrencygosync.Oncerequest mergingsingleflight
Go Programming World
Written by

Go Programming World

Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.