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.