Asynchrone Programmierung in Go: Von der Theorie zur Praxis
Go (Golang) ist bekannt für seine eingebaute Unterstützung asynchroner Programmierung. Im Gegensatz zu vielen anderen Sprachen, in denen Asynchronität komplexe Bibliotheken und Callbacks erfordert, bietet Go elegante und leistungsstarke Primitive: Goroutinen (goroutines) und Channels (channels). In diesem Leitfaden werden wir alle wichtigen Konzepte in der Praxis durchgehen, damit Sie effizienten, parallelen Code in Go schreiben können.
Was sind Goroutinen (Goroutines)?
Eine Goroutine ist ein leichtgewichtiger Ausführungsstrang, der von der Go-Laufzeitumgebung verwaltet wird. Im Gegensatz zu System-Threads verbrauchen Goroutinen sehr wenig Ressourcen (nur wenige Kilobyte Stack), sodass Tausende oder sogar Millionen von Goroutinen gleichzeitig ausgeführt werden können.
Eine Goroutine zu starten ist denkbar einfach – fügen Sie einfach das Schlüsselwort go vor einem Funktionsaufruf hinzu:
package main
import ( "fmt" "time")
func sayHello() { fmt.Println("Hallo aus der Goroutine!")}
func main() { go sayHello() // Starten der Goroutine time.Sleep(time.Millisecond * 100) // Der Goroutine Zeit zum Ausführen geben fmt.Println("Hauptfunktion beendet")}Beachten Sie: Wenn Sie time.Sleep nicht hinzufügen, könnte das Programm beendet werden, bevor die Goroutine mit der Ausführung beginnt. Für die Synchronisation sollten Sie besser andere Mechanismen verwenden, die wir weiter unten betrachten.
Channels: Datenübertragung zwischen Goroutinen
Channels sind typisierte „Röhren", über die Goroutinen Daten austauschen können. Ein Channel wird mit der Funktion make und dem Operator chan erstellt. Daten werden mit dem Operator <- in den Channel gesendet und mit <- links vom Channel daraus empfangen.
Betrachten wir ein Beispiel mit einem gepufferten und einem ungepufferten Channel:
package main
import "fmt"
func main() { // Ungepufferter Channel (Größe 0) ch := make(chan string) go func() { ch <- "Nachricht aus der Goroutine" // Daten senden }() msg := <-ch // Daten empfangen (blockiert, bis eine Nachricht ankommt) fmt.Println(msg) // Gepufferter Channel (Größe 2) bufferedCh := make(chan int, 2) bufferedCh <- 10 bufferedCh <- 20 fmt.Println(<-bufferedCh) // 10 fmt.Println(<-bufferedCh) // 20}Wichtig: Ein ungepufferter Channel blockiert den Sender, bis der Empfänger die Daten annimmt. Ein gepufferter Channel blockiert den Sender nur, wenn der Puffer voll ist.
Select: Multiplexing von Channels
Der Operator select ermöglicht es einer Goroutine, auf mehrere Channels gleichzeitig zu warten und denjenigen zu verarbeiten, der zuerst bereit ist. Dies ist das Analogon zu switch, aber für Channels. select ist ein mächtiges Werkzeug zum Erstellen von Timeouts, zum Abbrechen von Operationen und für nicht-blockierende Prüfungen.
package main
import ( "fmt" "time")
func main() { ch1 := make(chan string) ch2 := make(chan string) // Goroutine 1: sendet Daten nach 1 Sekunde go func() { time.Sleep(1 * time.Second) ch1 <- "Daten aus Channel 1" }() // Goroutine 2: sendet Daten nach 2 Sekunden go func() { time.Sleep(2 * time.Second) ch2 <- "Daten aus Channel 2" }() select { case msg1 := <-ch1: fmt.Println("Erhalten:", msg1) case msg2 := <-ch2: fmt.Println("Erhalten:", msg2) case <-time.After(500 * time.Millisecond): fmt.Println("Timeout! Kein Channel hat innerhalb von 500 ms geantwortet") }}In diesem Beispiel wird select aufgrund des Timeouts ausgelöst, da innerhalb von 500 ms kein Channel bereit ist. Wenn wir den Timeout entfernen würden, würde das Programm auf den ersten bereiten Channel warten.