Wenn du gerade mit Go anfängst, hörst du oft von Goroutines – aber was sind sie eigentlich? Und warum sind sie anders als normale OS Threads (Betriebssystem-Threads)?
In diesem Artikel erklären wir dir:
- Was OS Threads sind (ganz einfach)
- Was Goroutines sind
- Warum Goroutines in Go so beliebt sind
- Praktische Beispiele zum Ausprobieren
Alles auf einem Anfänger-Level, damit du direkt loslegen kannst!
1. Was sind OS Threads? (Die "Klassischen" Threads)
Ein OS Thread (Operating System Thread) ist eine Einheit, die vom Betriebssystem verwaltet wird. Jeder Thread hat:
- Seinen eigenen Stack (Speicherbereich für lokale Variablen)
- Seinen eigenen Programmzähler (wo im Code er gerade ist)
- Wird vom Betriebssystem geplant (Scheduler)
Ein Einfaches Beispiel mit OS Threads in Go
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
// Wie viele OS Threads Go verwenden kann
fmt.Printf("Anzahl OS Threads: %d\n", runtime.GOMAXPROCS(0))
var wg sync.WaitGroup
// Wir starten 5 OS Threads
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Thread %d läuft\n", id)
time.Sleep(1 * time.Second)
}(i)
}
wg.Wait()
fmt.Println("Alle Threads fertig!")
}
Was passiert hier?
-
runtime.GOMAXPROCS(0)zeigt dir, wie viele echte OS Threads Go verwenden kann - Standardmäßig ist das die Anzahl deiner CPU-Kerne
- Jede
go-Funktion läuft auf einem OS Thread
Problem: OS Threads sind schwer (heavyweight):
- Jeder Thread braucht viel Speicher (meist 1-2 MB pro Thread)
- Das Betriebssystem muss sie planen (Scheduling) – das kostet Zeit
- Du kannst nicht Tausende von OS Threads gleichzeitig haben
2. Was sind Goroutines? (Die "Leichten" Threads von Go)
Eine Goroutine ist eine leichte Funktion, die parallel laufen kann. Sie wird nicht vom Betriebssystem, sondern von Go selbst verwaltet.
Ein Einfaches Beispiel mit Goroutines
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// Wir starten 1000 Goroutines – das wäre mit OS Threads unmöglich!
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d läuft\n", id)
time.Sleep(100 * time.Millisecond)
}(i)
}
wg.Wait()
fmt.Println("Alle Goroutines fertig!")
}
Was ist der Unterschied?
- Goroutines sind super leicht: Nur 2-8 KB Speicher pro Goroutine
- Go verwaltet sie selbst: Der Go-Scheduler teilt OS Threads zwischen Goroutines auf
- Du kannst Millionen starten: Praktisch keine Grenze!
3. Der Wichtigste Unterschied: Speicherverbrauch
Lass uns das praktisch testen:
3.1. Test: Wie Viele OS Threads Können Wir Starten?
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
var count int
// Versuchen wir, 10.000 "Threads" zu starten
// (In Go sind das eigentlich Goroutines, die auf OS Threads laufen)
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(1 * time.Second)
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Printf("Anzahl fertiger Goroutines: %d\n", count)
fmt.Printf("Anzahl OS Threads verwendet: %d\n", runtime.GOMAXPROCS(0))
}
Ergebnis:
- Du kannst 10.000 Goroutines starten
- Aber Go verwendet nur 4-8 OS Threads (je nach CPU)
- Der Go-Scheduler teilt die Goroutines auf diese Threads auf
4. Wie Funktioniert der Go-Scheduler?
Der Go-Scheduler ist wie ein Manager, der entscheidet, welche Goroutine wann läuft:
OS Thread 1: [Goroutine A] → [Goroutine B] → [Goroutine C] → ...
OS Thread 2: [Goroutine D] → [Goroutine E] → [Goroutine F] → ...
OS Thread 3: [Goroutine G] → [Goroutine H] → [Goroutine I] → ...
Wie funktioniert das?
- Eine Goroutine läuft auf einem OS Thread
- Wenn sie blockiert (z.B. auf I/O wartet), gibt sie den Thread frei
- Eine andere Goroutine kann dann auf diesem Thread laufen
- So teilen sich Tausende Goroutines nur wenige OS Threads
Beispiel: Blockierende Operationen
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func fetchURL(url string, wg *sync.WaitGroup) {
defer wg.Done()
// Diese Operation blockiert (wartet auf Netzwerk)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Fehler bei %s: %v\n", url, err)
return
}
defer resp.Body.Close()
fmt.Printf("URL %s geladen: Status %d\n", url, resp.StatusCode)
}
func main() {
var wg sync.WaitGroup
urls := []string{
"https://www.google.com",
"https://www.github.com",
"https://www.stackoverflow.com",
}
// Alle URLs werden parallel geladen
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg)
}
wg.Wait()
fmt.Println("Alle URLs geladen!")
}
Was passiert hier?
- Jede
fetchURL-Goroutine blockiert, während sie auf die HTTP-Antwort wartet - Während sie wartet, kann eine andere Goroutine auf demselben OS Thread laufen
- So können wir viele Netzwerk-Anfragen parallel machen, ohne viele OS Threads zu brauchen
5. Praktischer Vergleich: Performance
Lass uns ein praktisches Beispiel machen, das den Unterschied zeigt:
5.1. Beispiel: Viele Zahlen Addieren
package main
import (
"fmt"
"sync"
"time"
)
// Funktion, die etwas "Arbeit" macht
func calculateSum(start, end int, result *int, wg *sync.WaitGroup) {
defer wg.Done()
sum := 0
for i := start; i < end; i++ {
sum += i
// Simuliere etwas Arbeit
time.Sleep(1 * time.Millisecond)
}
*result = sum
}
func main() {
start := time.Now()
var wg sync.WaitGroup
results := make([]int, 100)
// Starte 100 Goroutines
for i := 0; i < 100; i++ {
wg.Add(1)
go calculateSum(i*100, (i+1)*100, &results[i], &wg)
}
wg.Wait()
elapsed := time.Since(start)
total := 0
for _, r := range results {
total += r
}
fmt.Printf("Gesamtsumme: %d\n", total)
fmt.Printf("Zeit: %v\n", elapsed)
}
Warum ist das schnell?
- 100 Goroutines laufen parallel
- Sie teilen sich nur 4-8 OS Threads
- Der Go-Scheduler wechselt schnell zwischen ihnen
- Viel schneller als wenn wir alles nacheinander machen würden!
6. Wann Verwende Ich Was?
OS Threads (Direkt in Go selten nötig)
Du musst selten direkt mit OS Threads arbeiten in Go, weil:
- Goroutines sind einfacher zu verwenden
- Sie sind effizienter
- Go verwaltet sie automatisch
Aber: Manchmal brauchst du OS Threads für:
- C-Bibliotheken, die Threads erwarten
- Sehr spezielle System-Programmierung
Goroutines (Das Standard-Tool in Go)
Verwende Goroutines für:
- ✅ Parallele Netzwerk-Anfragen (HTTP, Datenbanken)
- ✅ Parallele Berechnungen
- ✅ Concurrent Programming (mehrere Dinge gleichzeitig)
- ✅ Worker Pools (viele Aufgaben parallel abarbeiten)
7. Ein Praktisches Beispiel: Worker Pool
Ein Worker Pool ist ein Muster, bei dem du eine begrenzte Anzahl von Goroutines hast, die viele Aufgaben abarbeiten:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d bearbeitet Job %d\n", id, job)
// Simuliere Arbeit
time.Sleep(500 * time.Millisecond)
results <- job * 2 // Ergebnis zurückgeben
}
}
func main() {
const numWorkers = 3 // Nur 3 Worker (Goroutines)
const numJobs = 10 // 10 Jobs zu erledigen
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Starte Worker (Goroutines)
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Sende Jobs
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Warte auf Ergebnisse
go func() {
wg.Wait()
close(results)
}()
// Zeige Ergebnisse
for result := range results {
fmt.Printf("Ergebnis: %d\n", result)
}
}
Was passiert hier?
- Wir haben nur 3 Worker-Goroutines
- Sie bearbeiten 10 Jobs nacheinander
- Jede Goroutine läuft auf einem OS Thread (geteilt)
- Viel effizienter als 10 separate OS Threads!
8. Häufige Fehler und Tipps
Fehler 1: Goroutines ohne Synchronisation
// ❌ FALSCH
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // Problem: i kann sich ändern!
}()
}
time.Sleep(1 * time.Second) // Nicht zuverlässig!
}
// ✅ RICHTIG
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println(id) // id ist eine Kopie
}(i)
}
wg.Wait() // Warte auf alle Goroutines
}
Fehler 2: Zu Viele Goroutines
// ❌ FALSCH: Millionen Goroutines starten
for i := 0; i < 10000000; i++ {
go doSomething()
}
// ✅ RICHTIG: Worker Pool verwenden
const maxWorkers = 100
jobs := make(chan int, 1000)
for w := 0; w < maxWorkers; w++ {
go worker(jobs)
}
9. Zusammenfassung: Die Wichtigsten Unterschiede
| Feature | OS Thread | Goroutine |
|---|---|---|
| Speicher | 1-2 MB | 2-8 KB |
| Verwaltung | Betriebssystem | Go Runtime |
| Anzahl | Hunderte | Millionen möglich |
| Scheduling | OS Scheduler | Go Scheduler |
| Geschwindigkeit | Langsamer (mehr Overhead) | Schneller (weniger Overhead) |
Einfach gesagt:
- OS Threads = Schwere Lastwagen (viel Kraft, aber teuer)
- Goroutines = Leichte Motorräder (schnell, effizient, viele gleichzeitig)
10. Nächste Schritte
Jetzt, wo du den Unterschied verstehst, kannst du:
- Mehr über Channels lernen (Kommunikation zwischen Goroutines)
- Select-Statements ausprobieren (mehrere Channels gleichzeitig)
- Context-Package verwenden (Goroutines abbrechen)
- Sync-Package erkunden (Mutex, WaitGroup, etc.)
Praktische Übungen:
- Baue einen einfachen Web-Scraper mit Goroutines
- Erstelle einen Worker Pool für Datei-Verarbeitung
- Implementiere parallele API-Aufrufe
Fazit
- Goroutines sind die "Superkräfte" von Go
- Sie sind viel leichter als OS Threads
- Du kannst viele gleichzeitig verwenden
- Der Go-Scheduler verwaltet sie automatisch
- Perfekt für parallele Programmierung!
Denk daran: In Go verwendest du fast immer Goroutines, nicht direkt OS Threads. Go macht das für dich!
Top comments (0)