This post was originally published on Medium on March 26, 2020, and migrated here with light edits — mostly refreshing links that pointed to the now-retired godoc.org.
This time, I’d like to introduce the package singleflight. If you haven’t used this package before, I hope you will consider it once.
What is singleflight?
Here’s what singleflight is below:
Package singleflight provides a duplicate function call suppression mechanism.
I consider singleflight to be a very useful package if your application is ever going to get multiple requests for the same resource. For example, RDB (master data), image files, IP address lookups, client certificates, and etc, are cases that need to refer to such resources that do not usually change.
Also, the singleflight method avoids the Thundering herd problem.
As an objective measure of how many packages actually use it: at the time of the original writing (March 26, 2020), it was imported by 154 packages — and that number has only grown since.
Especially, if you look at what kind of products use it, you can find Consul, Fabio, and use cases in the process of acquiring certificates among them.
There are three functions provided by singleflight:
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
func (g *Group) Forget(key string)
The actual source file is available at singleflight.go.
A recent trend of singleflight
If you look at the Go original code, singleflight is also found in Go’s internal package, and net/lookup.go is an example of a use case called inside Go. There are two flavors of the package:
It is maintained in a separate repository called Go Sync, which provides supplemental concurrency primitives in addition to the language and those provided by the “sync” and “sync/atomic” packages.
Introduction to use cases
Here, I’d like to introduce two use cases using the singleflight package.
Use case 1 — net/lookup
lookupGroup merges LookupIPAddr calls together for lookups for the same host. Then, LookupIPAddr uses that lookupGroup via a local resolver and refers to the host.
Here is the relevant code for the process (as of the original writing; line positions in net/lookup.go have shifted since):
// lookupGroup merges LookupIPAddr calls together for lookups for the same
// host. The lookupGroup key is the LookupIPAddr.host argument.
// The return values are ([]IPAddr, error).
lookupGroup singleflight.Group
func (r *Resolver) getLookupGroup() *singleflight.Group {
if r == nil {
return &DefaultResolver.lookupGroup
}
return &r.lookupGroup
}
// We don't want a cancellation of ctx to affect the
// lookupGroup operation. Otherwise if our context gets
// canceled it might cause an error to be returned to a lookup
// using a completely different context. However we need to preserve
// only the values in context. See Issue 28600.
lookupGroupCtx, lookupGroupCancel := context.WithCancel(withUnexpiredValuesPreserved(ctx))
lookupKey := network + "\000" + host
dnsWaitGroup.Add(1)
ch, called := r.getLookupGroup().DoChan(lookupKey, func() (interface{}, error) {
defer dnsWaitGroup.Done()
return testHookLookupIP(lookupGroupCtx, resolverFunc, network, host)
})
if !called {
dnsWaitGroup.Done()
}
Use case 2 — DataLayer and BFF in microservices
When we were developing with microservices, I think it is applicable to the following specifications.
Client
- For Microservice A, call the GetProduct, ListProducts API.
- The results received from the API are displayed as a list.
Microservice A — BFF layer
- Microservice A provides the API: “GetProductComponent.”
- The API “GetProductComponent” calls the two Microservice B’s APIs (GetProduct, ListProducts). After that, it aggregates the received information and returns the result as a “ProductComponent.”
Microservice B — Database layer
- Microservice B provides two APIs (GetProduct, ListProducts).
- The API “GetProduct” uses the ID and returns the master data of the “Product” as a result.
- The API “ListProducts” returns product information within a week of registering it in the Product master as a result.
When illustrated, the connection is as follows.

As described above, in this use case, the master information referred to in the API provided by Microservice B is considered to be a resource that does not change much, and by using singleflight on the Microservice A side of the caller, it is possible to consolidate duplicate API calls.
Conclusion
- The singleflight package provides a mechanism for bringing together duplicate function calls. If you need to tune an API that asks for the same resource, this can be very useful.
- If you’re using the BFF pattern in microservices, you may be able to find an easy-to-apply use case.
- If you are interested in singleflight, you may want to check how it is used in other products to understand it better.
That’s all there is to it. I’m glad if this article gave you the opportunity to give the singleflight package a try.