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")
}
output:
1
2
3
-> 3
-> 2
-> 1
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
}
}
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"))
}
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")
}
}
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")
}
go
Output:
1
-> 3
Recovered from: PANICCCC
-> 2
-> 1
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)
}
Output:
[we got an error here!]
defer: [no error here]
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)
}
Output:
Recovered from: we got an error here!
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")
}
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
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")
}
Output:
>>> Recovered from: PANICCC
Done
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")
}
Top comments (0)