Inhalt

Go routinen und Fehlerbehandlung

Bei einem kleinen Projekt an dem ich gerade arbeite habe ich gerade mit dem Thema Fehlerbeahndlung in Verbindung mit Goroutinen zu tun. Ich habe hier eine Library geschrieben, welche verschiedene Methoden für eine Bestellung anbietet. Natürlich habe ich brav darauf geachtet, dass im Fall der Fälle ein Fehler zurückgegeben wird. Aber wie ist das wenn ich nun eine Methode als Goroutine starte.

 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)
}

Code ausprobieren

Goroutinen erlauben parallele Ausführung

Schön und gut. Wenn wir uns jetzt vorstellen, dass wir tausende von Bestellungen bearbeiten müssen macht es natürlich Sinn über Effizienz nachzudenken. Indem wir unsere Methode als Gouroutine starten ermöglichen wir eine parallele Ausführung der Methoden. Klingt ja erstmal sinnvoll, also probieren wir das aus:

 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)
}

Code ausprobieren

Was passiert hier? Wir haben hier im Prinzip 3 Goroutinen. Zuerst die Goroutine auf der unser main() Funktion läuft. Dazu kommen noch die beiden Goroutinen für o.create(). Aber wir sehen keine Werte! Der Grund dafür ist einfach. Wir starten 2x o.create() als Goroutine. Das bedeutet aber auch, dass unsere main() weiterläuft, wir haben ja gesagt wir möchten o.create() jeweils parallel ausführen. Das führt aber jetzt dazu, dass die main() komplett durchläuft und nicht auf die Ergebnisse der anderen beiden Goroutinen wartet. Schlussendlich beendet sich sogar das Programm bevor wir einen Wert erhalten.

Der zweite Punkt ist, dass wir keine Fehlerbehandlung mehr machen. Verursacht eine der Goroutinen einen Fehler, so wird dieser nicht behandelt und der Benutzer bekommt eine wirklich häßliche Fehlermeldung mit der er wahrscheinlich nix anzufangen weiß.

Channels machen hier alles besser

Wir brauchen also 2 Dinge. 1. müssen wir dafür Sorgen, dass unser Hauptprogramm auf die Beendigung der Goroutinen wartet, 2. müssen wir dafür sorgen, dass wir von unseren Goroutinen eventuell Fehlermeldungen erhalten. Eine Lösung für beides bieten Channel. Ein Channel in Go ist im Grunde eine Verbindung von 2 Übergabepunkten, einem Sende und einem Empfänger. Ein wichtiger Punkt ist zu wissen, das Channels blockieren. Das bedeutetkonkret, dass wenn eine Go Routine Daten in einen Channel sendet, ihre weitere Ausführung so lange blockiert wird bis der Empfänger (das könnte eine andere Go Routine sein) die Daten entgegengenommen hat.

 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)
	}

}

Code ausprobieren

In diesem Beispiel iterieren wir über einen numbers und starten für jeden Wert eine Go Routine. Die Goroutine schreibt die Zahl in den channel. Der Wert des Channels wird jeweils von fmt.Printf() abgerufen. Das klappt soweit, jetzt schauen wir uns das blockieren einmal genauer an:

 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)
}

Code ausprobieren

Der einzige Unterschied zum vorherigen Code ist, dass fmt.Printf() nun außerhalb der for Schleife ist. Das bedeutet, dass wir den Channel nun nur noch einmal abrufen, statt zuvor fünf mal. Probiert man den Code aus so sieht man nur noch eine Ausgabe für ein Element von numbers und auch fmt.Printf(“end goroutine %v\n”, n) innerhalb der Go Routinen wird wird nur einmal ausgegeben. Was passiert hier? In der for Schleife starten wir die Go Routinen. Der erste Abruf des Chanels c erfolgt erst nachdem alle Go Routinen gestartet wurden. Hier kommt die blockierende Eigenschaft von Channels zum tragen. Dadurch, dass wir den Channel nur einmal abrufen bleiben die übrigen 4 Go Routinen nach dem senden an den Channel stehen und fmt.Printf(“end goroutine %v\n”, n) wird gar nicht ausgeführt. Ruft man aber 5 Sekunden später den Channel ein weiteres mal ab kann die nächste Go Routine beendet werden.

Channel für die Fehlerbehandlung nutzen

Das zweite Punkt unseres Ausgangsproblems war ja die Fehlerbehandlung. Im Grunde haben wir hierfür schon alles was wir brauchen. Wir definieren mit errch := make(chan error) einen Channel der Daten vom Typ error weiterleitet. Sollte ein Fehler auftreten senden wir diesen in den Channel (Zeile 44-47), falls nicht senden wir nil in den 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)
	}
}

Code ausprobieren

Auf der anderen Seite muss natürlich der Abruf der Daten aus dem Channel erfolgen. In Zeile 51 rufen wir die Daten innerhalb der for Schleife aus dem Channel ab. Dadurch, dass der Channel Daten vom Typ error weiterleitet können wir in den Zeilen 51-55 die Fehlerbehandlung so wie sonst auch handhaben 😊