Recovery from panic in Golang
What is a Panic?
As originally stated, Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.
In simple terms, a panic situation makes a function not carry out its expected execution, and can result in the whole program getting failed.
The Solution
Go natively provides some features that can help us recover from such a situation. Let's check them out,
Defer
Go’s defer
statement schedules a function call (the deferred function) to be run immediately before the function executing the defer
returns.
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}
Recover
When panic
is called, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way.
A call to recover
stops the unwinding and returns the argument passed to panic
. recover
is only useful inside deferred functions because the only code that runs while unwinding is inside deferred functions.
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
Implementing
Let's implement a simple mathematical function, that divides 2 numbers, and panics if the denominator is 0 Divide by zero error!
.
The below function checks the value of a number (denominator) and panics if it is 0.
func checkForError(y float64) {
if y == 0 {
panic("Divident cannot be 0! Divide by 0 error.")
}
}
This function is responsible for the actual division of provided numbers and returns the value. Meanwhile, it uses the function defined above to check for the denominator to be 0.
As checkForError
can break the flow, thus this function implements recover()
and defer
to return a 0 in case of panic.
func safeDivision(x, y float64) float64 {
var returnValue float64 defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occured:", err)
fmt.Println("Returning safe values")
returnValue = 0 }
}()
checkForError(y)
returnValue = x / y
return returnValue
}
Now, on combining
package mainimport (
"fmt"
)func main() {
fmt.Println("Pre panic execution")
value1 := safeDivision(2, 0)
fmt.Println("Post panic execution, -> ", value1) fmt.Println("Pre valid execution")
value2 := safeDivision(2, 1)
fmt.Println("Post valid execution, value -> ", value2)
}/*
This function is supposed to handle the errors
and return a safe value in case of an unusual situation
*/
func safeDivision(x, y float64) float64 {
var returnValue float64 defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occured:", err)
fmt.Println("Returning safe values")
returnValue = 0 }
}()
checkForError(y)
returnValue = x / y
return returnValue
}/*
This function is supposed to replicate the mathematical error
`DIVIDE BY ZERO`
and initiates a panic if the denominator is 0
*/
func checkForError(y float64) {
if y == 0 {
panic("Divident cannot be 0! Divide by 0 error.")
}
}
The output of the above program,