Recover is a built-in function that regains control of a panicking goroutine. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    panic("oops")
    fmt.Println("Returned normally from g.")
}

Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect.

https://go.dev/blog/defer-panic-and-recover

Why?

Firstly, we need to investigate what happens when we call recover(). When using recover(), the compiler will compile this statement into runtime.gorecover(). Let’s see the source code of this function:

func gorecover(argp uintptr) any {
	// Must be in a function running as part of a deferred call during the panic.
	// Must be called from the topmost function of the call
	// (the function used in the defer statement).
	// p.argp is the argument pointer of that topmost deferred function call.
	// Compare against argp reported by caller.
	// If they match, the caller is the one who can recover.
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

https://github.com/golang/go/blob/master/src/runtime/panic.go#L1038

Let’s analyze:
The program takes the pointer to the current goroutine via getg(). Pay attention to the attribute _panic. One of the conditions for recover() to be executed is that the _panic must be not nil. In the same way, _panic of current gorountine must be not nil.

func gopanic(e interface{}) {
    gp := getg()
    ...
    var p _panic   
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) 
        ...
}

We can see _panic is manipulated inside runtime.gopanic(), this is the function that is executed when we call panic(). So, if the current goroutine does not panic, its _panic is always nil. Then no matter how many times we call recover(), it will always return nil and do nothing.

Remind panic() function

When panic() is called, the program stops executing the current function immediately and begins unwinding the stack, which means it exits from the current function and goes up through the call stack, running deferred functions along the way. Before the program completely stops, any deferred functions in the stack (functions that were scheduled to run using defer) are executed

Conclusion

So. For recover() to work, the current goroutine must be panic + the recover() must be put inside the defer() func. If you directly call recover(), it will not work

References

https://groups.google.com/g/golang-nuts/c/KgPOzaMylHo/m/zX0GosvnAQAJ

https://github.com/golang/go/blob/master/src/runtime/panic.go#L1038

https://www.sobyte.net/post/2022-01/go-source-code-panic-and-recover/

Categorized in:

Tagged in:

,