Go routines and error handling

In a small project I am currently working on, I am dealing with the topic of error handling in goroutines. I have written a library here that offers different methods for orders. Of course, I took good care to ensure that an error is returned if the worst comes to the worst. But what is it like when I start a method as a goroutine?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
)

type order struct {
	id string
}

func (o order) create(id string) error {
	//do something useful with the order
	if id == "OD-002" {
		err := fmt.Errorf("error with %s", id)
		return (err) 
	}
	o.id = id
	return nil
}

func main() {
	o := order{}
	err := o.create("OD-001")
	fmt.Printf("Error: %v / Id: %s\n", err, o.id)
	
	o2 := order{}
	err = o.create("OD-002")
	fmt.Printf("Error: %v / Id: %s\n", err, o2.id)
}

Try it out

Go routines allow parallel execution

Nice and good. If we now imagine that we have to process thousands of orders, it obviously makes sense to think about efficiency. By starting our method as a go routine, we enable parallel execution of the methods. Sounds reasonable at first, so let’s try it out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
	"fmt"
)

type order struct {
	id string
}

func (o order) create(id string) error {
	//do something useful with the order
	if id == "OD-002" {
		err := fmt.Errorf("error with %s", id)
		return (err)
	}
	o.id = id
	return nil
}

func main() {
	o := order{}
	o2 := order{}
	
  //start goroutines
	go o.create("OD-001")
	go o.create("OD-002")
	
	fmt.Printf("Id: %s\n", o.id)
	fmt.Printf("Id: %s\n", o2.id)
}

Try it out

What’s happening? We basically have 3 goroutines here. First the goroutine on which our main() function runs. Then there are the two goroutines for o.create(). But we don’t see any values! The reason for this is simple. We start 2x o.create() as a go routine. But that also means that our main() continues running as we wanted to execute o.create() in parallel. However, this now leads to the main() running and finishing the program not waiting for the results of the other two go routines.

The second point is that we no longer do any error handling. If one of the go routines causes an error, it is not dealt with and the user gets a really ugly error message with which he probably doesn’t know what to do.

Channels make things better here

So we need 2 things. 1. we have to make sure that our main program is waiting for the go routines to finish, 2. we have to make sure that we receive error messages from our go routines. Channel offers a solution for both. A Channel in Go is basically a connection of 2 transfer points, a send and a receiver. An important point to know is that channels block. In concrete terms, this means that if a go routine sends data to a channel, its further execution is blocked until the recipient (which could be another go routine) has accepted the data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
	c := make(chan int)
	numbers := []int{1, 2, 3, 4, 5}
	for _, num := range numbers {
		go func(n int) {
			c <- n
			fmt.Printf("end goroutine %v\n", n)
		}(num)
		fmt.Printf("channel: %v\n", <-c)
	}

}

Try it out

In this example we iterate over numbers and start a go routine for each value. The go routine writes the number into the channel. The value of the channel is retrieved by fmt.Printf (). This works so far, now let’s take a closer look at blocking:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int)
	numbers := []int{1, 2, 3, 4, 5}
	for _, num := range numbers {
		go func(n int) {
			c <- n
			fmt.Printf("end goroutine %v\n", n)
		}(num)
	}
	fmt.Printf("channel: %v\n", <-c)
	time.Sleep(time.Second  5)
	fmt.Printf("channel: %v\n", <-c)
}

Try it out

The only difference from the previous code is that fmt.Printf() is now outside the for loop. This means that we now only retrieve the channel value once, instead of five times before. If you try out the code, you only see output for one element of numbers and also one fmt.Printf(“end goroutine% v \ n”, n) executed within the go routines. What’s happening? We start the go routines in the for loop. The first call of the Chanel c takes place after all go routines have been started. This is where the blocking property of channels comes into play. Because we only retireve from the channel once, the other 4 go routines remain paused after sending to the channel and fmt.Printf(“end goroutine% v \ n”, n) is not executed at all. If you retrieve from the channel again 5 seconds later, the next go routine can be end.

Channel for error handling

The second point of our initial problem was error handling. Basically, we already have everything we need for this. With errch:= make(chan error) we define a channel that forwards data of the type error. If an error occurs we send it to the channel (lines 44-47), if not we send nil to the channel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
	"fmt"
)

type order struct {
	id string
}

func (o order) create(id string) error {
	//tue etwas nützliches
	if id == "OD-002" {
		err := fmt.Errorf("error with %s", id)
		return (err)
	}
	o.id = id
	return nil
}

func main() {
	// a slice of order numbers
	orderno := []string{"OD-001", "OD-002", "OD-003"}
	// channel of type error
	errch := make(chan error)
	// a slice of empty orders
	orders := []order{
		{
			id: "",
		},
		{
			id: "",
		},
		{
			id: "",
		},
	}
	for i, order := range orders {
		go func() {
			err := order.create(orderno[i])
			if err != nil {
				errch <- err
				return
			}
			errch <- nil
		}()
		err := <-errch
		if err != nil {
			fmt.Println(err)
		}
		fmt.Println(order)
	}
}

Try it out

On the other hand, the data must be retrieved from the channel. In line 51 we retrieve the data within the for loop from the channel. Because the channel forwards data of the type error we can handle error handling in the lines 51-55 as usual 😊

0%