Understanding Exception Handling in Go singleflight

sorcererxw 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) {

	if c, ok := g.m[key]; ok {
		return c.val, c.err, true
	c := new(call)
	g.m[key] = c

	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

		if e, ok := c.err.(*panicError); ok {
			if len(c.chans) > 0 {
				// 为什么要单开一个进程 panic
				go panic(e)
				select {}
			} else {
		} 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.