Understanding Go's singleflight: A Simple, Practical Source Code Walkthrough
This article explains how Go's singleflight utility uses a lock and map to deduplicate concurrent calls, walks through its core structs and the Do method implementation, and shows how it improves system availability during traffic spikes.
Repository: https://github.com/gofish2020/easysingleflight
Data structures
The implementation relies on a lock and a map. The lock guarantees that only one request proceeds at a time, while the map stores a *call object per unique key so that identical requests can reuse the same result.
type (
Group struct {
calls map[string]*call // lazy initialization
mu sync.Mutex
}
)The call struct holds the outcome of a request and a channel used to notify waiting goroutines when the computation finishes.
type (
call struct {
done chan struct{} // closed when processing completes
val interface{} // result value
err error // error flag
}
)Do method flow
The public Do method implements the singleflight logic:
Lock the group and lazily create the calls map if it is nil.
If the supplied key already exists in g.calls, unlock, block on the existing call.done channel, then return the stored val, err, and true to indicate a cached result.
If the key is absent, allocate a new *call, initialize its done channel, store it in the map, and unlock.
Execute the provided function fn inside an anonymous function. A deferred recovery captures any panic, converting it to an error via newPanicError and setting val to nil.
After fn returns, reacquire the lock, delete the key from the map, unlock, and close call.done to unblock any waiting goroutines.
Return the computed val, err, and false to indicate the result was freshly computed.
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error, bool) {
// 1. lock and ensure map exists
g.mu.Lock()
if g.calls == nil {
g.calls = make(map[string]*call)
}
// 2. duplicate call – wait for existing result
if c, ok := g.calls[key]; ok {
g.mu.Unlock()
<-c.done // block until result is ready
return c.val, c.err, true
}
// 3. first caller – create call object
cl := new(call)
cl.done = make(chan struct{})
g.calls[key] = cl
// 4. unlock before executing fn
g.mu.Unlock()
func() {
// defer to handle panic and cleanup
defer func() {
if p := recover(); p != nil {
cl.err = newPanicError(p)
cl.val = nil
}
// 6. delete key from map
g.mu.Lock()
delete(g.calls, key)
g.mu.Unlock()
// 7. notify waiting goroutines
close(cl.done)
}()
// 5. execute fn
cl.val, cl.err = fn()
}()
return cl.val, cl.err, false
}The flow ensures that concurrent requests with the same key are coalesced into a single execution, improving system availability by preventing burst traffic from triggering circuit breakers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Nullbody Notes
Go backend development, learning open-source project source code together, focusing on simplicity and practicality.
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.
