🚀 Lehrpfad 3: Concurrency & Goroutines

Go 09: Leichtgewichtige Goroutines & sync.WaitGroup

Lerne nebenlĂ€ufige AusfĂŒhrung in Go: Starte Tausende leichtgewichtige Goroutines mit go und synchronisiere sie deterministisch mit sync.WaitGroup.

📂 Dateien dieser Lerneinheit:
📄 aufgabe.go (Startercode) đŸ§Ș test_aufgabe.go (Tests) 💡 musterloesung.go (Lösung) 📖 README.md

⚡ 1. Was sind Goroutines?

Eine Goroutine ist ein extrem leichtgewichtiger AusfĂŒhrungsfaden, der von der Go-Runtime verwaltet wird (M:N-Multiplexing auf OS-Threads). WĂ€hrend ein nativer OS-Thread ca. 1–2 MB Stack-Speicher reserviert, startet eine Goroutine mit nur ca. 2 KB dynamischem Stack. Dadurch kann ein einzelnes Go-Programm problemlos 100.000+ Goroutines gleichzeitig ausfĂŒhren!

[Main Goroutine] ───┬─── go Worker(1) ───> [Worker 1 lĂ€uft asynchron] ──┐
                    â”œâ”€â”€â”€ go Worker(2) ───> [Worker 2 lĂ€uft asynchron] ──┌──> wg.Wait() (Alle fertig!)
                    â””─── go Worker(3) ───> [Worker 3 lĂ€uft asynchron] ──┘
package main

import (
    "fmt"
    "time"
)

func sayHello(id int) {
    fmt.Printf("Worker %d meldet sich zum Dienst!\n", id)
}

func main() {
    // 🎯 Goroutine starten mit SchlĂŒsselwort 'go':
    go sayHello(1)
    go sayHello(2)

    // ACHTUNG: Ohne Synchronisation beendet sich main(), bevor Goroutines laufen!
    time.Sleep(10 * time.Millisecond) // Schlechter Stil – nutze sync.WaitGroup!
}

đŸ›Ąïž 2. Saubere Synchronisation mit sync.WaitGroup

Verwende niemals time.Sleep() zum Warten auf Goroutines. Die Standardbibliothek bietet dafĂŒr sync.WaitGroup mit exakt drei Methoden:

1. wg.Add(n)

Erhöht den internen ZÀhler um die Anzahl geplanter Goroutines.

2. defer wg.Done()

Dekrementiert den ZĂ€hler um 1, sobald eine Goroutine fertig ist.

3. wg.Wait()

Blockiert den Aufrufer (z.B. main), bis der ZĂ€hler exakt 0 erreicht.

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Signalisiert Fertigstellung beim Verlassen der Funktion
    fmt.Printf("Worker %d abgeschlossen\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        // ⚠ WICHTIG: WaitGroup IMMER als Pointer (*sync.WaitGroup) ĂŒbergeben!
        go worker(i, &wg)
    }

    wg.Wait() // Wartet deterministisch auf alle 3 Worker
    fmt.Println("Alle Worker erfolgreich beendet!")
}
🏱 Didaktische Analogie: Die Stempeluhr auf der Großbaustelle

Stell dir sync.WaitGroup wie eine digitale Stempeluhr vor: Vor Arbeitsbeginn bucht der Bauleiter 5 Handwerker ein (wg.Add(5)). Jeder Handwerker arbeitet eigenstĂ€ndig in seinem Raum. Sobald ein Handwerker fertig ist, stempelt er aus (wg.Done()). Der Bauleiter wartet am Ausgang (wg.Wait()) und schließt die Baustelle erst ab, wenn die Uhr genau 0 verbleibende Handwerker anzeigt.

📑 3. Cheat-Sheet: Best Practices fĂŒr Goroutines

Regel / Pattern Richtig (Idiomatisch) Falsch (Anti-Pattern)
Pointer bei WaitGroup func work(wg *sync.WaitGroup) func work(wg sync.WaitGroup) (Kopie fĂŒhrt zu Deadlock!)
Loop Variable Passing go func(val int) { ... }(i) go func() { use(i) }() (Race in Go < 1.22)
Done via defer defer wg.Done() ganz oben im Worker wg.Done() am Ende (kann bei Panic ĂŒbersprungen werden)
Add-Aufruf vor Goroutine wg.Add(1) vor dem go-Statement wg.Add(1) innerhalb der Goroutine (Race mit Wait)

🧠 Diagnostischer Selbst-Check

ÜberprĂŒfe dein VerstĂ€ndnis der Kernkonzepte und typischen Go-Fallen, bevor du die Übung in der Web-IDE bearbeitest.

🎯 Aufgaben & Checkliste in aufgabe.go

💡 Cloud-Architekt Tipp: Goroutine Leaks vermeiden

Jede gestartete Goroutine muss einen definierten Weg zur Beendigung haben. Eine Goroutine, die fĂŒr immer auf einen unbedienten Channel oder ein Lock wartet, belegt dauerhaft Speicher (Goroutine Leak).

đŸ’» In Web-IDE öffnen → Weiter zu Go 10: Channels →