Kurz: Go macht Nebenläufigkeit zum Sprachmerkmal. Das Motto: “Teile Speicher nicht durch Kommunikation, sondern kommuniziere, um Speicher zu teilen.”
Teil des Kurses Go
Go macht Nebenläufigkeit zum Sprachmerkmal. Das Motto: “Teile Speicher nicht durch Kommunikation, sondern kommuniziere, um Speicher zu teilen.”
Goroutinen
Eine Goroutine ist ein sehr leichtgewichtiger Ablauf (wenige KB). Du startest sie mit go:
package main
import (
"fmt"
"sync"
"time"
)
func arbeite(id int, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(time.Duration(id) * 10 * time.Millisecond)
fmt.Println("Goroutine", id, "fertig")
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go arbeite(i, &wg)
}
wg.Wait() // wartet, bis alle fertig sind
fmt.Println("alle fertig")
}Ausgabe:
Goroutine 1 fertig
Goroutine 2 fertig
Goroutine 3 fertig
alle fertigEndet main, endet das ganze Programm, auch wenn noch Goroutinen laufen. Deshalb wartet man mit sync.WaitGroup oder Channels.
Channels
Ein Channel transportiert Werte sicher zwischen Goroutinen:
package main
import "fmt"
func main() {
ch := make(chan int) // ungepuffert: Senden wartet auf Empfänger
go func() {
for i := 1; i <= 3; i++ {
ch <- i * i // senden
}
close(ch) // Ende signalisieren
}()
for wert := range ch { // empfangen, bis geschlossen
fmt.Println("empfangen:", wert)
}
puffer := make(chan string, 2) // gepuffert: bis zu 2 Werte ohne Empfänger
puffer <- "a"
puffer <- "b"
fmt.Println(len(puffer), cap(puffer), <-puffer, <-puffer)
v, ok := <-ch // aus geschlossenem Channel: Nullwert, ok = false
fmt.Println(v, ok)
}Ausgabe:
empfangen: 1
empfangen: 4
empfangen: 9
2 2 a b
0 false| Operation | Bedeutung |
|---|---|
ch <- x | senden |
x := <-ch | empfangen |
x, ok := <-ch | empfangen, ok == false wenn geschlossen und leer |
close(ch) | schließen (nur der Sender!) |
for x := range ch | bis zum Schließen lesen |
chan<- T, <-chan T | nur senden / nur empfangen (Typen in Signaturen) |
Worker-Pool
Ein klassisches Muster: Viele Aufgaben, wenige Worker.
package main
import (
"fmt"
"sort"
"sync"
)
func worker(id int, aufgaben <-chan int, ergebnisse chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for n := range aufgaben {
ergebnisse <- n * n
}
}
func main() {
aufgaben := make(chan int, 10)
ergebnisse := make(chan int, 10)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, aufgaben, ergebnisse, &wg)
}
for n := 1; n <= 9; n++ {
aufgaben <- n
}
close(aufgaben)
go func() { wg.Wait(); close(ergebnisse) }() // schließen, wenn alle Worker fertig sind
var liste []int
for r := range ergebnisse {
liste = append(liste, r)
}
sort.Ints(liste)
fmt.Println(liste)
}Ausgabe:
[1 4 9 16 25 36 49 64 81]select: auf mehrere Channels warten
package main
import (
"fmt"
"time"
)
func main() {
a := make(chan string)
b := make(chan string)
go func() { time.Sleep(30 * time.Millisecond); a <- "von A" }()
go func() { time.Sleep(10 * time.Millisecond); b <- "von B" }()
for i := 0; i < 2; i++ {
select {
case m := <-a:
fmt.Println(m)
case m := <-b:
fmt.Println(m)
case <-time.After(time.Second):
fmt.Println("Zeitüberschreitung")
}
}
ch := make(chan int)
select {
case v := <-ch:
fmt.Println(v)
default: // wenn nichts bereit ist: nicht blockieren
fmt.Println("nichts bereit")
}
}Ausgabe:
von B
von A
nichts bereitGemeinsame Daten schützen
Wenn mehrere Goroutinen dieselbe Variable ändern, entsteht ein Datenwettlauf. Lösungen:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var mu sync.Mutex
var geschuetzt int
var atomar atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for k := 0; k < 10000; k++ {
mu.Lock()
geschuetzt++
mu.Unlock()
atomar.Add(1)
}
}()
}
wg.Wait()
fmt.Println(geschuetzt, atomar.Load())
}Ausgabe:
40000 40000| Werkzeug | Zweck |
|---|---|
sync.Mutex, sync.RWMutex | gegenseitiger Ausschluss |
sync/atomic | atomare Zähler und Flags |
sync.WaitGroup | auf mehrere Goroutinen warten |
sync.Once | genau einmal ausführen |
sync.Map | threadsichere Map |
| Channels | Daten übergeben statt teilen |
Datenwettläufe findest du mit go run -race main.go oder go test -race.
context: Abbruch und Zeitlimits
package main
import (
"context"
"fmt"
"time"
)
func langeArbeit(ctx context.Context) error {
for i := 0; ; i++ {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled oder context.DeadlineExceeded
case <-time.After(20 * time.Millisecond):
}
}
}
func main() {
ctx, abbrechen := context.WithTimeout(context.Background(), 70*time.Millisecond)
defer abbrechen()
fmt.Println(langeArbeit(ctx))
ctx2, stop := context.WithCancel(context.Background())
go func() { time.Sleep(30 * time.Millisecond); stop() }()
fmt.Println(langeArbeit(ctx2))
}Ausgabe:
context deadline exceeded
context canceledGib jeder Funktion, die lange läuft oder Netzwerk benutzt, als ersten Parameter einen ctx context.Context.
Regeln
- Wer startet, sorgt für das Ende: Jede Goroutine braucht einen klaren Abbruch (Channel,
context) - Nur der Sender schließt einen Channel
- Goroutinen nicht unbegrenzt starten (Worker-Pool oder
errgroup) - Gemeinsamen Zustand vermeiden; wenn nötig Mutex/atomic
-raceimmer in Tests laufen lassen
Merke
go f()startet eine Goroutine;sync.WaitGroupwartet- Channels (
chan T) übertragen Werte;close,range,select - Datenwettläufe vermeiden mit Mutex, atomic oder Channels; Test mit
-race contextfür Abbruch und Zeitlimits- Goroutinen sind billig, aber nicht kostenlos: begrenzen und beenden
Übungsaufgabe
Berechne mit einem Worker-Pool aus vier Goroutinen die Summe der Quadrate von 1 bis 1000.
Quiz zur Selbstkontrolle
Wie startet man eine Goroutine?
- go funktion() (richtig)
- async funktion()
- thread funktion()
- spawn funktion()
Wer darf einen Channel schließen?
- Der Sender (richtig)
- Der Empfänger
- Jeder
- Der Compiler
Wofür ist select?
- Auf mehrere Channel-Operationen gleichzeitig warten (richtig)
- Zum Auswählen von Dateien
- Für SQL
- Für Schleifen
Weiter im Kurs
Zurück: Interfaces und Generics
Weiter: Pakete, Module und Standardbibliothek
Alle Kapitel: Go im Überblick