Zum Inhalt springen
Go Anfänger 35 min

Goroutines & Channels

Nebenlaeufigkeit in Go: Starte tausende Goroutines leicht und synchronisiere sie ueber Channels. Das Herzstueck von Go's Concurrency-Modell.

Aktualisiert:
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 erwartet
  • wg.Done() - eine fertig
  • wg.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 ch liest bis zum Ende
  • select wartet auf mehrere Channels und erlaubt Timeouts
  • sync.WaitGroup fuer einfache “warte bis alles fertig”-Szenarien
  • -race nutzen!

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.

Zurück zum Go Kurs