Panic and Recover in Go Programming




Welcome to part 21 of the Go programming tutorial, where we will be covering panic and recover functionalities in golang.

The idea of panic is to halt the program and start to panic. This will stop the running function and run all of the deferred functions from the panicking function. The recover function lets you recover from a panicking goroutine. To recover a panicking goroutine, you would need to use recover from within one of the deferred functions of that goroutine. First, let's panic. Here's the code we'll start with up to this point from our coverage of defer:

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func say(s string) {
	defer wg.Done()
	for i := 0; i < 3; i++ {
		time.Sleep(100*time.Millisecond)
		fmt.Println(s)
	}	
}

func main() {
	wg.Add(1)
	go say("Hey")
	wg.Add(1)
	go say("there")
	wg.Wait()
}

Now, of course, in our say function, I am sure you are all wondering: "Hey man, what if the i in say is 2?! Wouldn't that just be horrible!?" ... and you're right! We need to handle for that with a panic!

func say(s string) {
	defer wg.Done()
	for i := 0; i < 3; i++ {
		time.Sleep(100*time.Millisecond)
		fmt.Println(s)
		if i == 2 {
			panic("Oh dear... a 2")
		}
	}	
}

Now, we can run our program:

Hey
there
Hey
there
Hey
panic: Oh dear... a 2

goroutine 5 [running]:
main.say(0x4b2432, 0x3)
        C:/Users/H/Desktop/golangstuff/gotut21-panic-recover.go:17 +0x15d
created by main.main
        C:/Users/H/Desktop/golangstuff/gotut21-panic-recover.go:24 +0x6e
exit status 2

As you can see, we correctly panicked about reaching 2, but what if we don't want the program to just stop entirely? Well, then we can use recover. In order to use recover, we need to use it from a deferred function. In our case, the only function that we're deferring is wg.Done(), but this is not our function, so we need to make our own. Let's make a new function called cleanup(), and then in here we can handle the panic by checking for a recover:

if r := recover(); r != nil {
		fmt.Println("Recovered in cleanup:", r)
	}

Note, we defined r and then did the comparison operator to check the value in the same line. We've not covered doing this. There's how you can do it. We'd like to save recover() to r so we can later reference r. The r, or return from recover() is the argument that we pass to panic.

Now, we can defer the cleanup function:

func say(s string) {
	defer wg.Done()
	...

Full code up to this point:

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

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

func say(s string) {
	defer wg.Done()
	defer cleanup()
	for i := 0; i < 3; i++ {
		time.Sleep(100*time.Millisecond)
		fmt.Println(s)
		if i == 2 {
			panic("Oh dear... a 2")
		}
	}	
}

func main() {
	wg.Add(1)
	go say("Hey")
	wg.Add(1)
	go say("there")
	wg.Wait()
}

Running this gives:

Hey
there
Hey
there
Hey
Recovered in cleanup: Oh dear... a 2
there
Recovered in cleanup: Oh dear... a 2

While it is fine to have stacked defer statements, I think that, in our case, it's not necessary. The wg.Done() is part of "cleaning up," so let's just combine these:

func cleanup() {
	if r := recover(); r != nil {
		fmt.Println("Recovered in cleanup:", r)
	}
	wg.Done()
}

func say(s string) {
	defer cleanup()
	for i := 0; i < 3; i++ {
		time.Sleep(100*time.Millisecond)
		fmt.Println(s)
		if i == 2 {
			panic("Oh dear... a 2")
		}
	}	
}

This gives us the same result, but produces slightly more read-able code, since defers go in reverse order that they come in, and reading through line-by-line with stacked defers can mean lots of bouncing around. Finally, this also just plain makes logical sense to me. Full code up to this point:

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func cleanup() {
	if r := recover(); r != nil {
		fmt.Println("Recovered in cleanup:", r)
	}
	wg.Done()
}

func say(s string) {
	defer cleanup()
	for i := 0; i < 3; i++ {
		time.Sleep(100*time.Millisecond)
		fmt.Println(s)
		if i == 2 {
			panic("Oh dear... a 2")
		}
	}	
}

func main() {
	wg.Add(1)
	go say("Hey")
	wg.Add(1)
	go say("there")
	wg.Wait()
}

Now, we're going to cover Go Channels, which we can use to connect concurrent goroutines in order to send and receive values between them.

The next tutorial:





  • Introduction to the Go Programming Language
  • Go Language Syntax
  • Go Language Types
  • Pointers in Go Programming
  • Simple Web App in Go Programming
  • Structs in the Go Programming Language
  • Methods in Go Programming
  • Pointer Receivers in Go Programming
  • More Web Dev in Go Language
  • Acessing the Internet in Go
  • Parsing XML with Go Programming
  • Looping in Go Programming
  • Continuing our Go Web application
  • Mapping in Golang
  • Mapping Golang sitemap data
  • Golang Web App HTML Templating
  • Applying templating to our Golang web app
  • Goroutines - Concurrency in Goprogramming
  • Synchronizing Goroutines - Concurrency in Golang
  • Defer - Golang
  • Panic and Recover in Go Programming
  • Go Channels - Concurrency in Go
  • Go Channels buffering, iteration, and synchronization
  • Adding Concurrency to speed up our Golang Web Application