Goroutines & Channels
Nebenlaeufigkeit in Go: Starte tausende Goroutines leicht und synchronisiere sie ueber Channels. Das Herzstueck von Go's Concurrency-Modell.
Inhaltsverzeichnis
Goroutines & Channels
Go wurde gebaut, um moderne, nebenlaeufige Server-Software einfach zu schreiben. Das zentrale Werkzeug dafuer sind Goroutines und Channels. Zusammen bilden sie ein Modell, das oft CSP (Communicating Sequential Processes) genannt wird.
Das Motto: “Kommuniziere durch Teilen, teile nicht durch Kommunikation.”
Goroutines
Eine Goroutine ist eine nebenlaeufig laufende Funktion. Starten ist trivial:
func hallo() {
fmt.Println("Hallo aus der Goroutine!")
}
func main() {
go hallo() // laeuft im Hintergrund
time.Sleep(100 * time.Millisecond)
fmt.Println("Ende von main")
}
Ohne go laeuft hallo() synchron. Mit go startet die Funktion parallel.
Wie leicht sind Goroutines?
Sehr. Eine Goroutine braucht nur ein paar KB Stack (verglichen mit MB fuer OS-Threads). Go multiplexed tausende Goroutines auf wenige OS-Threads - ein Programm mit 100.000+ Goroutines ist keine Seltenheit.
Anonyme Goroutines
Oft nutzt man inline-Funktionen:
go func() {
fmt.Println("Anonym parallel")
}()
Die () am Ende ruft die Funktion direkt auf.
Das Problem: Synchronisation
func main() {
go fmt.Println("Hallo")
// Oops - main endet, bevor die Goroutine ausfuehrt
}
Wenn main endet, wird das gesamte Programm beendet - auch laufende Goroutines. Wir brauchen einen Weg, auf ihr Ende zu warten. Channels loesen das.
Channels
Ein Channel ist eine typisierte Leitung, ueber die Goroutines Werte austauschen.
Channel erstellen
ch := make(chan int) // unbuffered
gepuffert := make(chan int, 5) // bufferd, Kapazitaet 5
Senden und Empfangen
ch <- 42 // senden
wert := <-ch // empfangen
Der Pfeil zeigt immer in Richtung des Datenflusses.
Unbuffered Channels - blockieren
Ein unbuffered Channel blockiert den Sender, bis jemand empfaengt - und umgekehrt:
func main() {
ch := make(chan string)
go func() {
ch <- "Hallo" // blockt bis main empfaengt
}()
nachricht := <-ch // blockt bis Sender schickt
fmt.Println(nachricht)
}
Das ist gleichzeitig Kommunikation und Synchronisation.
Buffered Channels
Ein buffered Channel blockt erst, wenn der Puffer voll ist:
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 wuerde blockieren
Beispiel: Mehrere Worker
package main
import (
"fmt"
"time"
)
func verarbeite(id int, job int, ergebnisse chan<- int) {
time.Sleep(time.Second)
ergebnisse <- job * 2
}
func main() {
ergebnisse := make(chan int, 5)
for i := 1; i <= 5; i++ {
go verarbeite(i, i, ergebnisse)
}
for i := 0; i < 5; i++ {
fmt.Println("Ergebnis:", <-ergebnisse)
}
}
Die 5 Goroutines laufen parallel. Jede schreibt ihr Ergebnis in den Channel. Wir lesen die 5 Ergebnisse wieder raus - in zufaelliger Reihenfolge, je nach dem, wer zuerst fertig ist.
Chan-Richtung
chan<- int= “kann nur senden”<-chan int= “kann nur empfangen”
Das ist eine nette Sicherheit, wenn du Channels an Funktionen uebergibst.
close und range
Wenn du fertig bist mit Senden, schliesse den Channel:
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for wert := range ch {
fmt.Println(wert)
}
// 1, 2, 3 - for range endet, wenn der Channel geschlossen ist
}
Lesen aus einem geschlossenen Channel funktioniert weiter (Zero Value), schreiben in einen geschlossenen Channel panic’t.
Ist der Channel zu?
Erkennst du mit dem comma-ok-Pattern:
wert, ok := <-ch
if !ok {
fmt.Println("Channel ist zu")
}
select - das switch fuer Channels
select laesst dich auf mehrere Channels gleichzeitig warten:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() { time.Sleep(1 * time.Second); ch1 <- "von 1" }()
go func() { time.Sleep(2 * time.Second); ch2 <- "von 2" }()
for i := 0; i < 2; i++ {
select {
case m := <-ch1:
fmt.Println(m)
case m := <-ch2:
fmt.Println(m)
}
}
}
Timeout mit select
Ein haeufiges Pattern:
select {
case ergebnis := <-ch:
fmt.Println("Bekommen:", ergebnis)
case <-time.After(5 * time.Second):
fmt.Println("Timeout!")
}
Nicht-blockierendes Senden/Empfangen
Mit default:
select {
case ch <- 42:
fmt.Println("gesendet")
default:
fmt.Println("Channel war voll, ueberspringe")
}
sync.WaitGroup - eine Alternative
Wenn du nur warten willst, bis N Goroutines fertig sind, ist WaitGroup oft einfacher als Channels:
import "sync"
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d fertig\n", id)
}(i)
}
wg.Wait() // blockt bis alle Done() gerufen haben
fmt.Println("Alle fertig!")
}
wg.Add(n)- n Goroutines erwartetwg.Done()- eine fertigwg.Wait()- blockt bis Zaehler = 0
Was du noch wissen solltest
Race Conditions
Gleichzeitiger Schreibzugriff auf dieselbe Variable ohne Synchronisation ist ein Data Race. Go findet sie fuer dich:
go run -race .
go test -race ./...
Nutze -race beim Entwickeln haeufig.
Wann Channels, wann Mutex?
- Channels: Wenn Daten zwischen Goroutines fliessen
sync.Mutex: Wenn ein einzelner Zustand geschuetzt werden muss
Beide sind gueltige Werkzeuge. Beginn mit Channels - sie passen oft besser zur Struktur.
Zusammenfassung
go f()startet eine leichtgewichtige, parallele Funktion- Channels (
make(chan T)) synchronisieren und kommunizieren - Unbuffered Channels sind Rendezvous - blockieren bis beide Seiten da sind
close(ch)beendet den Channel;for range chliest bis zum Endeselectwartet auf mehrere Channels und erlaubt Timeoutssync.WaitGroupfuer einfache “warte bis alles fertig”-Szenarien-racenutzen!
Das war dein Einstieg in Go’s Nebenlaeufigkeits-Modell. Weitere Themen wie Context, Timeouts, worker pools und das Testen von nebenlaeufigem Code findest du in den fortgeschrittenen Kapiteln.