Nebenläufigkeit mit Goroutinen
Goroutinen sind einfach Link to heading
Jede Prozedur, also Funktion, die kein Ergebnis zurückgibt, kann als Goroutine gestartet werden, indem man vor den Aufruf das Schlüsselwort Go schreibt:
go render("das ist eine Goroutine")Eine Goroutine sollte mit return verlassen werden können, denn sie kann auch weiter laufen, wenn das Hauptprogramm schon beendet wurde. Meist macht eine Goroutine, die nicht mit dem Hauptprogramm oder anderen Goroutinen kommuniziert, auch wenig Sinn. Zur Kommunikation zwischen Hauptprogramm und Goroutinen dienen Channels (Schlüsselwort chan). Wie Kollektionen (slices, structs, maps, etc.) werden channels mit der Funktion make erstellt:
senke := make(chan int) // ein Kanal, der eine Ganzzahl transportiert
senke <- 42 // sendet die Zahl 42 in den Kanal "senke"
// eine andere routine
func DoAnything(senke chan int) {
// eine Routine wartet, bis der Kanal senke eine Zahl sendet
gotAnything:= <- senke // danach läuft die Routine weiter
...
}Auf diese Weise lassen sich Goroutinen und Hauptprogramm synchronisieren und werden angehalten, bis für sie benötigte Daten über einen Channel bereit gestellt werden können, ohne dass Rechenzeit verschwendet wird. Das Konzept erinnert an Message-Loops bei anderen Architekturen, ist aber flexibler, weil man noch mehr damit anfangen kann. Auch können Goroutinen auch wirklich nebenläufig ausgeführt werden, und die im Kanal transportierten Daten benötigen keinen Mutex, weil simultaner Zugriff auf sie verhindert ist. Sollte man dennoch simultan auf Variablen zugreifen müssen, verwendet man natürlich auch bei Go Mutexe.
Jedenfalls kann man auch mit Goroutinen und Channels gut Anwendungsschichten modellieren, die sich über Channels austauschen. Anwendungsserver empfangen etwa Aufrufe via HTTP Befehlszeile und filtern in mehreren Middleware-Schichten Parameter heraus, erledigen Authentifizierung, suchen Daten in Datenbanken und bauen schliesslich eine aus allen angeforderten Daten eine Webseite zusammen und liefern diese an den Absender des Requests aus.
So etwas kann man mit Goroutinen und Channels gut lösen. Das folgende Beispiel zeigt einen Client, der zwei Goroutinen-Schichten besitzt, um in einer Schicht Daten von einem JSON-Webservice zu holen, um sie mit einer anderen Goroutine stückweise verzögert auf der Konsole auszugeben.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// ein freier Zitate-Webserver
const Api = "https://type.fit/api/quotes"
// Datenmodell für JSON Zitat
type Quote struct {
Text string `json:"text"`
Autor string `json:"author"`
}
// der JSON Server liefert ein Array aus Zitaten
type Quotes []Quote
// Service Zugriffs Goroutine
// api: die URL des Zitate-Servers
// ch: Go-Channel für Quelle
//
func request(api string, ch chan Quote) {
var quotes Quotes
// Zitate per HTTP GET abholen
resp, err := http.Get(api)
if err != nil {
fmt.Println("Failed to fetch quote:", err)
ch <- Quote{"", ""} // signalisiere Abbruch
return
}
// beendet nach Verlassen der Funktion die Server-Verbindung
// das ist auch eine Nebeneffekt der Go - Channel-Architektur
defer resp.Body.Close()
// liest den Body des Get-Requests ein
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Print("Failed to read response body:", err)
ch <- Quote{"", ""}
return
}
// dekodiert die JSON Daten in das Go Datenmodell (Datentyp Quote)
err = json.Unmarshal([]byte(body), "es) // call by reference
if err != nil {
fmt.Println("Failed to parse response body:", err)
ch <- Quote{"", ""}
return
}
// sende die Zitate einzeln in den Quellen-Kanal
for _, q := range quotes {
ch <- q
}
// melde Daten-Ende
ch <- Quote{"", ""}
}
// Ausgabe - Goroutine
// ch: Go-Channel für Senke
//
func render(ch chan Quote) {
count := 1 // Zitate - Zähler
// Schleife wird durch leeres Zitat beendet
for {
q := <-ch // empfangen eines Zitats
// wenn kein weiteres Zitat -> beende Goroutine
if len(q.Text) == 0 {
fmt.Println("no more quotes today")
break
}
// gib Zitat aus
fmt.Printf("%d: %s - %s\n", count, q.Text, q.Autor)
count++
// warte 2 Sekunden
time.Sleep(2 * time.Second)
}
}
func main() {
// öffne Quell-Kanal
quelle := make(chan Quote)
// starte Server-Abfrage
go request(Api, quelle)
// öffne Senken-Kanal
senke := make(chan Quote)
// starte Ausgabeschleife
go render(senke)
for {
// übergib Zitat an Ausgabeschleife
q := <-quelle
// wenn keine weiteren Zitate, stoppe Ausgabe
if len(q.Text) == 0 {
senke <- q
break
}
// sende Zitat an Ausgabe
senke <- q
}
}