DEV Community

Yana
Yana

Posted on • Edited on

Defer & Panic Recovery In Go

Image description
Panics are caused by operations like accessing elements outside the bounds of an array, null dereference, closing closed channels, and so on. They are abnormal operations that should be avoided by checks in the code, but sometimes these scenarios are missed, and the panic can crash our application.
There might be many reasons to recover from a panic, the most obvious being to "fail gracefully" by finishing cleanup operations and properly reporting the errors before exiting.

Panic recovery in Go is based on defers, lets take a look at how they work.

Each time the function encounters a defer statement, it's pushed onto a stack. Once the function exits, these defer statements run in LIFO (Last In, First Out) order.

func deferOrdering() {
    defer fmt.Println(" -> 1")
    defer fmt.Println(" -> 2")
    defer fmt.Println(" -> 3")

    fmt.Println(" 1")
    fmt.Println(" 2")
    fmt.Println(" 3")
}
Enter fullscreen mode Exit fullscreen mode

output:

 1
 2
 3
 -> 3
 -> 2
 -> 1

Enter fullscreen mode Exit fullscreen mode

A classic example for defer usage is closing the file after using it. At the end of this function, we should close the file we opened, no matter where we decide to exit the function due to possible errors.

func fileClose() {
    f, _ := os.Create("learning_go.txt")

    defer f.Close()

    f.Write([]byte("some garbage text"))

    if err := func1(); err != nil{
        return
    }

    if err := func2(); err != nil{
        return
    }

    if err := func3(); err != nil{
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to add additional logic to the defer statement, we can transform the one line defer into a function:

func fileCloseCheckError() {
    f, _ := os.Create("learning_go.txt")

    // longer defer function
    defer func() {
        err := f.Close()
        if err != nil {
            fmt.Println("oops... we failed")
        }
    }()

    f.Write([]byte("some garbage text"))
}
Enter fullscreen mode Exit fullscreen mode

The defer function can be extracted into a separate function for better readability:

func fileCloseCheckErrorDeferFunction() {
    f, _ := os.Create("learning_go.txt")

    defer closeFunc(f)

    f.Write([]byte("some garbage text"))
}

func closeFunc(f *os.File) {
    err := f.Close()
    if err != nil {
        fmt.Println("oops... we failed")
    }
}
Enter fullscreen mode Exit fullscreen mode

The recover() function is used to recover from panic. In order to execute it after a panic, we need to place the recover() inside a defer statement.

Defer always executes upon exiting the function, no matter if the function is returning success, failure or panics. In this example, we can see that the function panicked before it finished printing the last two lines. After the panic, all defers were executed in reverse order, even the ones that did not implement panic recovery.

func deferOrderingWithPanic() {
    defer fmt.Println(" -> 1")
    defer fmt.Println(" -> 2")

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()

    // first defer does not recover panic
    defer fmt.Println(" -> 3")

    fmt.Println(" 1")

    panic("PANICCCC")

    fmt.Println(" 2")
    fmt.Println(" 3")
}
Enter fullscreen mode Exit fullscreen mode


go
Output:

 1
 -> 3
Recovered from: PANICCCC
 -> 2
 -> 1
Enter fullscreen mode Exit fullscreen mode

You can check out the actual implementation of panic in runtime/panic.go. All defers on current stack are executed in subsequent order, and if the panic does not recover, we exit on exit(2) in fatalpanic func after printing the stack trace.

When you pass a pointer into a defer function, and change this pointer later on, the defer function keeps the value it received when the defer statement was reached.

func deferInputPointer() {
    str := []string{"no error here"}

    defer fmt.Printf("defer: %s\n", str)

    str = []string{"we got an error here!"}

    fmt.Println(str)
}

Enter fullscreen mode Exit fullscreen mode

Output:

[we got an error here!]
defer: [no error here]

Enter fullscreen mode Exit fullscreen mode

recover() function receives the element passed inside the panic() function:

func deferInputPointerPanicRecover() {
    str := "no error here"

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()

    str = "we got an error here!"

    panic(str)
}
Enter fullscreen mode Exit fullscreen mode

Output:

Recovered from: we got an error here!
Enter fullscreen mode Exit fullscreen mode

A very important, and slightly unexpected behavior to be familiar with, if how panics behave inside the goroutines.

If like me, you have previously worked with languages like java, you would probably expect to be able to have some general recovery statement in the main flow, to be able to catch any possible unexpected panics not covered by your logic, even if they occur inside the goroutines. Sadly, this does not apply for go. It is not possible to recover a panic outside the goroutine.

A blog post from Rob Pike on the philosophy on panics in go.

That obviously means that every third party repo you use in your code can potentially crash your code if it creates a new goroutine that panics without recovery.

Here is an example where we create a goroutine, and add the recovery outside it's scope. This causes the application to crash.

func goroutinePanicRecoveryFails() {
    ids := []string{"id1", "id2", "id3"}

    wg := sync.WaitGroup{}
    wg.Add(3)

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()

    for _, id := range ids {
        go func(id string) {
            defer wg.Done()

            if id == "id2" {
                panic("PANICCC")
            }
        }(id)
    }

    wg.Wait()

    fmt.Println("Done")
}
Enter fullscreen mode Exit fullscreen mode

Output:

panic: PANICCC

goroutine 22 [running]:
main.goroutinePanicRecoveryFails.func2({0x1001f843d?, 0x0?})
        ~/GolandProjects/awesomeProject/cmd/example.go:217 +0xa0
created by main.goroutinePanicRecoveryFails in goroutine 1
        ~/GolandProjects/awesomeProject/cmd/example.go:212 +0xb0

Process finished with the exit code 2
Enter fullscreen mode Exit fullscreen mode

To recover from panic, let's add a defer inside the same goroutine:

func goroutinePanicRecoverySuccess() {
    ids := []string{"id1", "id2", "id3"}

    wg := sync.WaitGroup{}
    wg.Add(3)

    for _, id := range ids {
        go func(id string) {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Println(">>> Recovered from:", r)
                }
            }()

            defer wg.Done()

            if id == "id2" {
                panic("PANICCC")
            }
        }(id)
    }

    wg.Wait()

    fmt.Println("Done")
}
Enter fullscreen mode Exit fullscreen mode

Output:

>>> Recovered from: PANICCC
Done
Enter fullscreen mode Exit fullscreen mode

Copy-pasting the recovery code in in the beginning of every gorutine you create seems like a bad practice. Let's explore one way to implement a generic wrapper for a panic recovery inside a goroutine.

This function would receive a struct containing all the needed parameters we previously used in our gorutine, and implement the executesFunc interface - implementation of the goroutine we previously had. wrappedPanicHandle would contain the panic recovery logic, and execute the gorutine with all the needed params.

func main() {
    func1El := func1{}
    go wrappedPanicExec(&func1El)

    func2El := func2{1, "", true}
    go wrappedPanicExec(&func2El)
}

func wrappedPanicExec(e executesFunc) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println(">>> Recovered from:", r)
        }
    }()

    e.execute()
}

type executesFunc interface {
    execute()
}

type func1 struct {
}

func (f1 *func1) execute() {
    fmt.Println("execute func 1")
}

type func2 struct {
    param1 int
    param2 string
    param3 bool
}

func (f2 *func2) execute() {
    if f2.param3 {
        f2.param2 = (string)(f2.param1)
    }

    fmt.Println("execute func 2")
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)