Firmino Changani

I shall recover - Golang

In this blog post, I intend to demonstrate the usage of Go’s built-in function recover() used to gracefully handle panics.

The example I prepared is effectively a function that takes another function as an argument and repeatedly executes it based on the time interval passed in the first parameter.

 1package main
 2
 3import "time"
 4
 5func main() {
 6	runOnInterval(time.Millisecond*500, func(ranAt time.Time) {
 7		iWillPanic()
 8	})
 9}
10
11func iWillPanic() {
12	panic("something went wrong")
13}

The implementation of runOnInterval() is rather simple, it makes use of a for/loop that blocks on every iteration based in the time interval provided.

1func runOnInterval(interval time.Duration, handler func(ranAt time.Time)) {
2	ticker := time.NewTicker(interval)
3
4	for {
5		handler(time.Now())
6		<-ticker.C
7	}
8}

Such a small program panics every time it’s ran, and it outputs the following information on the terminal:

1panic: something went wrong
2
3goroutine 1 [running]:
4main.iWillPanic(...)

A naive assumption…

What if the function passed in runOnInterval() is not meant to panic? And that perhaps if something unexpected were to happen runOnInterval() should at least retry it three times before stopping the execution altogether. That’s when the recover() function enters the conversation, and its basic syntax can be defined as such:

 1package main
 2
 3func iWillPanic() {
 4	panic("Yeah, I refuse to run")
 5}
 6
 7func main() {
 8	defer func() {
 9		if r := recover(); r != nil {
10			log.Println("Oops, something went wrong: ", r)
11		}
12	}()
13	iWillPanic()
14}

recover must be called within a deferred function. When the enclosing function panics, the defer will activate and a recover call within it will catch the panic. - Go by Example: Recover

Now let’s recover

 1package main
 2
 3import (
 4	"time"
 5	"log"
 6)
 7
 8func runOnInterval(interval time.Duration, handler func(ranAt time.Time)) {
 9	ticker := time.NewTicker(interval)
10	for {
11		func () {
12			defer func() {
13				if r := recover(); r != nil {
14					log.Printf("The handler has panicked: %v", r)
15				}
16			}()
17			handler(time.Now())
18		}()
19		<-ticker.C
20	}
21}

The use of defer inside a loop is discouraged, but runOnInterval still needs to check whether the handler has panicked and to do that I wrapped both the recover and the handler inside an anonymous function that is going check whether the handler has panicked or not. Here are the logs shown in the terminal after the change made above:

2023/07/05 08:25:39 The handler has panicked: something went wrong
2023/07/05 08:25:40 The handler has panicked: something went wrong
2023/07/05 08:25:40 The handler has panicked: something went wrong
2023/07/05 08:25:41 The handler has panicked: something went wrong
2023/07/05 08:25:41 The handler has panicked: something went wrong
2023/07/05 08:25:42 The handler has panicked: something went wrong
2023/07/05 08:25:42 The handler has panicked: something went wrong
2023/07/05 08:25:43 The handler has panicked: something went wrong
2023/07/05 08:25:43 The handler has panicked: something went wrong

It works, but the implementation is rather incomplete because runOnInterval will attempt to recover forever, which is not ideal because there is no guarantee that whatever is making the handler panic will ever change. As a countermeasure, a counter - no pun intended - comes in quite handy:

 1package main
 2
 3import (
 4	"time",
 5	"log"
 6)
 7
 8func runOnInterval(interval time.Duration, handler func(ranAt time.Time)) {
 9	panicCounter := 0
10	ticker := time.NewTicker(interval)
11
12	for {
13		if panicCounter == 3 {
14			log.Printf("The handler has panicked %d times. Ending execution", panicCounter)
15			break
16		}
17
18		func() {
19			defer func() {
20				if r := recover(); r != nil {
21					panicCounts++
22					log.Printf("The handler has panicked. Panic count: %d", panicCounter)
23				}
24			}()
25			handler(time.Now())
26		}()
27
28		<-ticker.C
29	}
30}
31
32func main() {
33	runOnInterval(time.Millisecond*500, func(ranAt time.Time) {
34		iWillPanic()
35	})
36}
37
38func iWillPanic() {
39	panic("something went wrong")
40}

Here are the logs that the program outputs with the introduction of the counter:

2023/07/05 08:29:33 The handler has panicked. Panic count: 1
2023/07/05 08:29:34 The handler has panicked. Panic count: 2
2023/07/05 08:29:34 The handler has panicked. Panic count: 3
2023/07/05 08:29:35 The handler has panicked 3 times. Ending execution

That’s it for this blog post, and thank you for passing by.

#golang

Reply to this post by email ↪