golang.org/x/sync/singleflight can prevent multiple Goroutines from doing the same thing simultaneously and repeatedly.
Its source code is very simple, and the overall process is as follows:
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error) {
g.mu.Lock()
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait()
...
return c.val, c.err, true
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err
}
Whenever a request comes in:
- If it is the first time, encapsulate it as a call object and put it into the request pool.
- Afterwards, all identical requests simply wait for the original call to finish and directly use its result.
Compared to the larger process, the doCall function in singleflight is more interesting:
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
normalReturn := false // 是否成功运行完成
recovered := false // 是否发生 panic
defer func() {
if !normalReturn && !recovered {
c.err = errGoexit // 既没有成功运行,也没有 panic,那么就只有可能是 runtime.Goexit
}
g.mu.Lock()
defer g.mu.Unlock()
if e, ok := c.err.(*panicError); ok {
if len(c.chans) > 0 {
// 为什么要单开一个进程 panic
go panic(e)
select {}
} else {
panic(e)
}
} else if c.err == errGoexit {
} else {
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
}
}()
func() { // 在闭包内直接使用 defer,使代码更加简洁
defer func() {
if !normalReturn {
if r := recover(); r != nil {
c.err = newPanicError(r)
}
}
}()
c.val, c.err = fn()
normalReturn = true
}()
if !normalReturn {
recovered = true
}
}
- Why does the situation of recovered == false but normalReturn ==false occur?
There are two ways to immediately interrupt a goroutine in Go:
- panic: Errors can be caught and recovered using recover. If there is no recover, it will cause the entire process to exit.
- runtime.Goexit: Immediately terminates the running of the current goroutine.
The common feature of both is: after they end, the defer on their stack will still be executed in order. So, it is possible to judge whether the function ends normally or a recover occurs in defer. If neither happens, a runtime.Goexit occurs.