Asynchrone Programmierung in Go: Ein vollständiger Leitfaden für Anfänger
Go (Golang) wurde mit Blick auf die Anforderungen der modernen Entwicklung entwickelt, bei der Leistung und Parallelität eine Schlüsselrolle spielen. Im Gegensatz zu vielen Sprachen, in denen Asynchronität durch Callbacks oder Promises implementiert wird, bietet Go eingebaute und elegante Mechanismen: Goroutinen und Kanäle. Dieser Leitfaden führt Sie in die Grundlagen der asynchronen Programmierung in Go ein und zeigt, wie Sie effizienten nebenläufigen Code schreiben.
Was sind Goroutinen und wie unterscheiden sie sich von Threads?
Eine Goroutine ist ein leichtgewichtiger Ausführungsstrang, der von der Go-Laufzeitumgebung verwaltet wird. Im Gegensatz zu System-Threads des Betriebssystems verbrauchen Goroutinen weitaus weniger Ressourcen: Ihr Stack beginnt mit wenigen Kilobyte und kann dynamisch wachsen und schrumpfen. Dies ermöglicht es, Millionen von Goroutinen gleichzeitig zu starten, ohne den Speicher wesentlich zu belasten.
Eine Goroutine zu starten ist sehr 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() // Goroutine starten time.Sleep(100 * time.Millisecond) // Warten, damit die Goroutine ausgeführt werden kann fmt.Println("Hauptfunktion beendet")}Wichtig zu beachten: Wenn die Hauptfunktion main() endet, werden alle gestarteten Goroutinen zwangsweise gestoppt. Für die Synchronisation verwenden wir spezielle Werkzeuge.
Synchronisation mit WaitGroup
Der einfachste Weg, auf die Beendigung von Goroutinen zu warten, ist die Verwendung von sync.WaitGroup. Es funktioniert wie ein Zähler: Wir erhöhen ihn beim Start einer Goroutine und verringern ihn bei deren Beendigung. Die Hauptfunktion blockiert, bis der Zähler null erreicht.
package main
import ( "fmt" "sync" "time")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Zähler bei Beendigung verringern fmt.Printf("Arbeiter %d hat Arbeit begonnen", id) time.Sleep(time.Second) fmt.Printf("Arbeiter %d hat Arbeit beendet", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Zähler vor dem Start erhöhen go worker(i, &wg) }
wg.Wait() // Auf Beendigung aller Goroutinen warten fmt.Println("Alle Arbeiter haben ihre Arbeit beendet")}Beachten Sie: wg.Add(1) wird vor dem Start der Goroutine aufgerufen, und wg.Done() innerhalb derselben (normalerweise über defer). Dies stellt sicher, dass der Zähler auch bei einer Panik korrekt verringert wird.
Kanäle: Kommunikation zwischen Goroutinen
Kanäle (channels) sind typisierte „Röhren", über die Goroutinen sicher Daten austauschen können. Kanäle bieten standardmäßig Synchronisation: Das Senden in einen Kanal blockiert die Goroutine, bis eine andere Goroutine den Wert liest, und umgekehrt.
Kanäle werden mit der Funktion make erstellt:
package main
import "fmt"
func main() { // Erstellen eines ungepufferten Kanals für ganze Zahlen ch := make(chan int)
// Starten einer Goroutine, die Daten sendet go func() { ch <- 42 // Wert in den Kanal senden }()
// Aus dem Kanal lesen (blockiert, bis ein Wert ankommt) value := <-ch fmt.Println("Erhaltener Wert:", value)}Kanäle gibt es in zwei Arten:
- Ungepufferte – synchron, das Senden blockiert bis zum Empfang.
- Gepufferte – asynchron, das Senden blockiert nur, wenn der Puffer voll ist.
// Erstellen eines gepufferten Kanals für 3 ElementebufferedCh := make(chan string, 3)bufferedCh <- "A"bufferedCh <- "B"bufferedCh <- "C" // Blockiert nicht, solange er nicht voll ist