for ist das einzige Schleifen-Keyword in Go. Es gibt drei Formen: die Schleife mit drei Klauseln for i := 0; i < n; i++, die Schleife nur mit Bedingung for cond, die in Go die while-Schleife ist, und die Endlosschleife for { ... }. Mit range iteriert dasselbe Keyword über Slices, Arrays, Maps, Strings, Channels, Integer und Iterator-Funktionen. Go hat kein while, do, foreach oder loop als Keyword (Go spec, For statements).
Kurzfassung
for init; cond; post { ... }ist die Zählschleife. Keine runden Klammern um die Klauseln, und die geschweiften Klammern sind Pflicht.for cond { ... }ist eine while-Schleife, undfor { ... }läuft, bis einbreak,returnoderpanicsie beendet.for i, v := range xiteriert über einen Slice, ein Array, eine Map, einen String, einen Channel, einen Integer (Go 1.22) oder eine Iterator-Funktion (Go 1.23). Die Reihenfolge bei Maps ist zufällig.- Seit Go 1.22 bekommt jede Iteration eine neue Schleifenvariable, wenn
go.modgo 1.22oder neuer deklariert. Goroutines, die in einer Schleife gestartet werden, sehen nicht mehr alle den letzten Wert. - Der range-Wert ist eine Kopie. Weise
items[i]zu, um den Slice zu ändern. - Ein
breakohne Label innerhalb einesselectoderswitchverlässt nur diese Anweisung. Verwende ein Label, um die Schleife zu verlassen.
Welche Formen hat eine for-Schleife in Go?
Die Spezifikation definiert eine einzige for-Anweisung mit drei Formen. Sie unterscheiden sich nur darin, welche Klauseln du schreibst.
Die Schleife mit drei Klauseln
Die Init-Anweisung läuft einmal, die Bedingung wird vor jeder Iteration geprüft, und die Post-Anweisung läuft nach jeder Iteration. Diese Schleife wiederholt einen unzuverlässigen Health Check mit exponentiellem Backoff:
example.gogopackage main import ( "errors" "fmt" "time" ) var errUnavailable = errors.New("503 service unavailable") func fetchStatus(attempt int) error { if attempt < 3 { return errUnavailable } return nil } func main() { backoff := 100 * time.Millisecond for attempt := 1; attempt <= 5; attempt++ { err := fetchStatus(attempt) if err == nil { fmt.Printf("attempt %d: ok\n", attempt) break } fmt.Printf("attempt %d: %v, retrying in %v\n", attempt, err, backoff) time.Sleep(backoff) backoff *= 2 } }
example.texttextattempt 1: 503 service unavailable, retrying in 100ms attempt 2: 503 service unavailable, retrying in 200ms attempt 3: ok
attempt existiert nur innerhalb der Schleife. Jede der drei Klauseln darf fehlen. for ; attempt <= 5; { ist gültig, und gofmt schreibt es zu for attempt <= 5 { um.
Die Gewohnheit aus C, die Klauseln in runde Klammern zu setzen, kompiliert nicht:
example.gogofor (i := 0; i < 3; i++) {
example.texttext./main.go:6:9: syntax error: unexpected :=, expected )
Die Schleife nur mit Bedingung (while)
Lässt du Init- und Post-Anweisung weg, wird for zur while-Schleife. Diese hier arbeitet eine Job-Queue ab, bis sie leer ist:
example.gogopackage main import "fmt" func main() { queue := []string{"resize:42.png", "resize:43.png", "thumbnail:44.png"} for len(queue) > 0 { job := queue[0] queue = queue[1:] fmt.Println("processing", job) } fmt.Println("queue empty") }
example.texttextprocessing resize:42.png processing resize:43.png processing thumbnail:44.png queue empty
Schreibst du stattdessen while, bekommst du einen Parse-Fehler, weil while in Go ein gewöhnlicher Bezeichner ist:
example.texttext./main.go:7:8: syntax error: unexpected name pending at end of statement
Der Compiler liest while als Ausdruck, findet danach einen weiteren Namen und gibt auf. Die Meldung sagt nichts über Schleifen, und deshalb verwirrt sie Leute, die aus anderen Sprachen kommen.
Die Endlosschleife
Ganz ohne Klauseln läuft for { ... }, bis etwas im Inneren die Schleife verlässt. Server verwenden diese Form für Accept-Loops und Poller:
example.gogofor { conn, err := ln.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return } log.Printf("accept: %v", err) continue } go handle(conn) }
Die Schleife endet, wenn der Listener geschlossen wird und Accept net.ErrClosed zurückgibt. for {} ersetzt while (true) und for (;;), und der Compiler weiß, dass die Schleife nie von selbst endet. Eine Funktion, die mit for {} ohne break endet, braucht kein abschließendes return.
Wie funktioniert for range?
range macht aus einer for-Schleife eine Iteration über einen Wert. Was du zurückbekommst, hängt vom Typ ab:
| Range über | Erster Wert | Zweiter Wert |
|---|---|---|
| Slice oder Array | Index | Kopie des Elements |
| String | Byte-Offset | Rune (Unicode-Codepoint) |
| Map | Key | Value |
| Channel | empfangener Wert | keiner |
Integer n (Go 1.22) | 0 bis n-1 | keiner |
| Iterator-Funktion (Go 1.23) | was sie liefert | was sie liefert |
Du kannst die zweite Variable weglassen (for i := range items) oder die erste mit _ ignorieren (for _, item := range items). for range n ohne Variablen führt einen Block n-mal aus.
example.gogopackage main import ( "fmt" "maps" "slices" ) func main() { endpoints := []string{"/health", "/orders", "/users"} for i, path := range endpoints { fmt.Println(i, path) } statusCounts := map[int]int{200: 812, 404: 17, 500: 3} for _, code := range slices.Sorted(maps.Keys(statusCounts)) { fmt.Printf("%d: %d\n", code, statusCounts[code]) } for offset, r := range "café" { fmt.Printf("%d:%c ", offset, r) } fmt.Println() for attempt := range 3 { fmt.Print(attempt, " ") } fmt.Println() results := make(chan string, 2) results <- "job 1 done" results <- "job 2 done" close(results) for msg := range results { fmt.Println(msg) } }
example.texttext0 /health 1 /orders 2 /users 200: 812 404: 17 500: 3 0:c 1:a 2:f 3:é 0 1 2 job 1 done job 2 done
Bei der Map-Schleife solltest du genau hinschauen. Die Runtime randomisiert die Iteration über Maps absichtlich, damit sich Code nicht auf eine Reihenfolge verlassen kann. Um die Statuscodes sortiert auszugeben, liefert maps.Keys einen Iterator über die Keys, und slices.Sorted sammelt und sortiert sie. Das Go-Keyword map behandelt die Iteration über Maps ausführlicher.
Die String-Schleife liefert Runes, und die Offsets sind Byte-Positionen. Deshalb beginnt é bei Offset 3 und belegt zwei Bytes. Verwende for i := 0; i < len(s); i++ mit s[i], wenn du die rohen Bytes brauchst.
Die Channel-Schleife endet, wenn der Channel geschlossen wird. Schließt ihn niemand, blockiert die Schleife für immer, sobald der Buffer leer ist. Das Go-Keyword chan klärt, wer einen Channel schließen sollte.
Iterator-Funktionen (Go 1.23) lassen jede Funktion mit der Signatur func(yield func(K, V) bool) mit range zusammenarbeiten. Die Standardbibliothek gibt sie aus maps.Keys, maps.All, slices.All, slices.Backward und strings.Lines zurück, und du kannst eigene schreiben, etwa für paginierte API-Ergebnisse oder Datenbankzeilen. range hat mehr Regeln, als hier Platz haben. Der Abschnitt zu range im Überblick über die Go-Keywords ist die Kurzfassung.
Was hat sich in Go 1.22 bei Schleifenvariablen geändert?
Vor Go 1.22 deklarierte eine for-Schleife ihre Variablen einmal und verwendete sie in jeder Iteration wieder. Eine Closure oder Goroutine, die die Variable erfasste, sah den Wert, den sie hatte, wenn die Closure lief, und das war meist der letzte. Go 1.22 hat das geändert, sodass jede Iteration eine frische Variable bekommt (Go blog, Fixing For Loops in Go 1.22).
Dieser Health Checker startet eine Goroutine pro Datenbank-Host:
example.gogopackage main import ( "fmt" "sync" ) func main() { hosts := []string{"db-1", "db-2", "db-3"} var wg sync.WaitGroup for _, host := range hosts { wg.Add(1) go func() { defer wg.Done() fmt.Println("checking", host) }() } wg.Wait() }
Mit go 1.21 in go.mod gaben drei Läufe jeweils dreimal checking db-3 aus, und go vet meldete:
example.texttextmain.go:16:28: loop variable host captured by func literal
Mit go 1.27 in go.mod prüft derselbe Code alle drei Hosts in der Reihenfolge, in der die Goroutines fertig werden, und go vet hat nichts zu beanstanden.
Die Umstellung hängt an der go-Zeile in go.mod, nicht an der Toolchain, mit der du baust. Ein Go-1.27-Compiler baut ein Modul, das go 1.21 deklariert, weiterhin mit der alten Semantik. Eine Dependency, die vor 1.22 geschrieben wurde, funktioniert also weiter so, wie ihre Autoren sie getestet haben. Die Änderung gilt sowohl für die Form mit drei Klauseln als auch für range. Der alte Workaround host := host in der Schleife ist jetzt überflüssig. Der Modernizer forvar in go fix löscht ihn für dich. Das Go-Keyword func behandelt Closures und erfasste Variablen ausführlicher.
Warum ändert sich der Slice nicht, wenn ich den range-Wert ändere?
Die zweite range-Variable ist eine Kopie des Elements. Bei einem Slice von Structs lässt eine Änderung an einem Feld der Kopie den Slice unberührt:
example.gogopackage main import "fmt" type Invoice struct { ID string Amount int Paid bool } func main() { invoices := []Invoice{ {ID: "inv-101", Amount: 4200}, {ID: "inv-102", Amount: 1800}, } for _, inv := range invoices { inv.Paid = true } fmt.Println(invoices[0].Paid, invoices[1].Paid) for i := range invoices { invoices[i].Paid = true } fmt.Println(invoices[0].Paid, invoices[1].Paid) }
example.texttextfalse false true true
Die erste Schleife setzt Paid auf einer Kopie, die am Ende jeder Iteration verworfen wird. Die zweite Schleife greift per Index in den Slice und ändert das echte Element. Go 1.22 hat daran nichts geändert. Seit 1.22 bekommt zwar jede Iteration ihre eigene Variable, aber diese Variable ist immer noch eine Kopie. Bei einem Slice von Pointern ([]*Invoice) ist die Kopie ein Pointer, und inv.Paid = true aktualisiert die Rechnung tatsächlich.
Einen Pointer auf ein Element behalten
&invoices[i] liefert dir einen Pointer in das Backing Array des Slice. Dieser Pointer veraltet, wenn der Slice später über seine Kapazität hinaus wächst, weil append die Elemente in ein neues Array kopiert:
example.gogovar large *Invoice for i := range invoices { if invoices[i].Amount > 4000 { large = &invoices[i] } } invoices = append(invoices, Invoice{"inv-103", 900}) large.Amount = 0 fmt.Println(invoices[0].Amount, large.Amount)
example.texttext4200 0
large zeigt immer noch in das alte Array, also kommt der Schreibzugriff nie bei invoices an. Speichere den Index (largeIdx = i) statt eines Pointers, wenn der Slice wachsen könnte.
Wie funktionieren break und continue in einer for-Schleife?
break verlässt das innerste for, switch oder select. continue springt zur nächsten Iteration des innersten for. Beide akzeptieren optional ein Label, das eine äußere Schleife benennt.
Ein Label ist ein Bezeichner mit einem Doppelpunkt dahinter, der in der Zeile vor der Schleife steht. Dieser Scan stoppt beim ersten Fehler in mehreren Logdateien:
example.gogopackage main import "fmt" func main() { logLines := [][]string{ {"INFO start", "INFO ready"}, {"INFO request", "ERROR db timeout", "INFO retry"}, {"INFO shutdown"}, } files: for file, lines := range logLines { for _, line := range lines { if line[:5] == "ERROR" { fmt.Printf("file %d: first error %q\n", file, line) break files } } } }
example.texttextfile 1: first error "ERROR db timeout"
Ohne das Label würde break nur die innere Schleife verlassen, und der Scan würde mit der dritten Datei weitermachen. continue files würde den Rest der aktuellen Datei überspringen und zur nächsten gehen.
break innerhalb von select
Dieser Bug kompiliert und sieht beim schnellen Lesen richtig aus. Ein Worker liest Jobs, bis sein Context abgebrochen wird:
example.gogofunc worker(ctx context.Context, jobs <-chan string) { for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) break case job := <-jobs: fmt.Println("handled", job) } } fmt.Println("worker flushed metrics") }
Das break verlässt das select, nicht das for. Der Worker gibt worker stopping aus und geht direkt zurück ins select, wo ctx.Done() immer noch geschlossen ist. Also gibt er dieselbe Zeile in einer engen Schleife immer wieder aus. Die Metriken werden nie geschrieben. go vet meldet den toten Code nach der Schleife:
example.texttextmain.go:20:2: unreachable code
und staticcheck meldet das break selbst:
example.texttextmain.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)
Die Lösung ist ein Label an der Schleife:
example.gogofunc worker(ctx context.Context, jobs <-chan string) { loop: for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) break loop case job := <-jobs: fmt.Println("handled", job) } } fmt.Println("worker flushed metrics") }
example.texttexthandled email:1 handled email:2 worker stopping: context deadline exceeded worker flushed metrics
Ein return im Case ctx.Done() funktioniert auch, wenn nach der Schleife nichts mehr zu tun ist. Dieselbe Falle gilt für break in einem switch innerhalb einer Schleife.
Häufige Fehler mit for-Schleifen
defer in einer Schleife
defer läuft, wenn die Funktion zurückkehrt, nicht wenn die Iteration endet. Eine Schleife, die eine Datei öffnet und Close per defer aufschiebt, hält jede Datei offen, bis die gesamte Funktion fertig ist:
example.gogofor _, p := range paths { f, err := os.Open(p) if err != nil { return 0, err } defer f.Close() // ... }
Bei einer Handvoll Pfaden merkt man davon nichts, aber bei zehntausend Logdateien gehen dem Prozess die File Descriptors aus. Weder go vet noch staticcheck melden das. Verschieb den Schleifenkörper in eine Funktion, damit jedes defer am Ende seines eigenen Aufrufs läuft, oder ruf f.Close() direkt am Ende der Iteration auf. Häufige Go-Fehler und wie du sie vermeidest zeigt diesen Fall mit einem vollständigen Beispiel.
An einen Slice anhängen, während range über ihn läuft
range wertet seinen Operanden einmal aus, vor der ersten Iteration. Ein append in der Schleife lässt sie nicht länger laufen:
example.gogourls := []string{"/", "/about"} for _, u := range urls { urls = append(urls, u+"/sitemap.xml") } fmt.Println(len(urls), urls)
example.texttext4 [/ /about //sitemap.xml /about/sitemap.xml]
Die Schleife lief zweimal, einmal pro ursprünglichem Element. Das macht sie sicher, bedeutet aber auch, dass ein Crawler auf diese Weise keine neuen URLs entdecken kann. Verwende eine Bedingungsschleife über eine Queue wie for len(queue) > 0 weiter oben, wenn die Arbeitsliste unterwegs wächst.
Eine Map während range ändern
Den aktuellen Key zu löschen, während range über eine Map läuft, ist erlaubt und sicher. Diese Schleife entfernt inaktive Sessions:
example.gogofor id, idle := range sessions { if idle > 30 { delete(sessions, id) } }
Keys während der Schleife hinzuzufügen ist ebenfalls erlaubt, aber laut Spezifikation kann ein neuer Eintrag „während der Iteration erzeugt oder übersprungen werden“ („may be produced during the iteration or may be skipped“). Sammle neue Keys in einer separaten Map und führe sie danach zusammen, wenn jeder einzelne verarbeitet werden muss.
Die Adresse der Schleifenvariable speichern
ptrs = append(ptrs, &inv) innerhalb von for _, inv := range invoices liefert dir Pointer auf Kopien, nicht auf die Elemente des Slice. Vor Go 1.22 zeigte jeder Pointer auf dieselbe Variable und damit auf die letzte Rechnung. Seit Go 1.22 zeigt jeder Pointer auf seine eigene Kopie. Das ist korrekt, aber immer noch vom Slice getrennt. Verwende &invoices[i], wenn du die Originale ändern willst, und denk an die Warnung zur Kapazität weiter oben.
Gibt es in Go eine while- oder do-while-Schleife?
Nein. Effective Go sagt, die for-Schleife von Go „vereint for und while, und es gibt kein do-while“. Die Form nur mit Bedingung deckt ab, wofür andere Sprachen while verwenden.
Ein do-while führt den Schleifenkörper einmal aus, bevor die Bedingung geprüft wird. In Go schreibst du das als Endlosschleife mit der Prüfung am Ende. Paginierte API-Aufrufe sind der typische Fall, weil du die erste Seite immer abrufst:
example.gogocursor := "" for { page := fetchPage(cursor) fmt.Println("fetched", page.Items) cursor = page.Next if cursor == "" { break } }
example.texttextfetched [order-1 order-2] fetched [order-3]
Das sind zwei Zeilen mehr, als do { } while (cursor != "") bräuchte, und die Abbruchbedingung steht am Ende, wo man sie erwartet.
Wo LevelUpGo ins Spiel kommt
LevelUpGo bringt dir Go mit Übungen bei, die echten Go-Code im Browser ausführen. Go Basics hat zwei Lektionen zu for: eine zu den drei Schleifenformen und eine zu range, break und continue. Concurrency Fundamentals behandelt die Schleifen, die in nebenläufigem Code Probleme machen, etwa Goroutines in einer Schleife starten, range über Channels und eine for/select-Schleife bei einem Abbruch verlassen. Die anderen 24 reservierten Wörter findest du in Go Keywords: Alle 25 erklärt.
Häufig gestellte Fragen
Hat Go eine while-Schleife?
Nein, Go hat kein Keyword while. Schreib for cond { ... } für eine while-Schleife und for { ... } für eine Endlosschleife. Wenn du while tippst, bekommst du einen Syntaxfehler, weil der Compiler es als gewöhnlichen Namen behandelt.
Hat Go eine foreach-Schleife?
for ... range ist das foreach von Go. for _, v := range items besucht jedes Element eines Slice, und dieselbe Form funktioniert für Arrays, Maps, Strings, Channels, Integer und Iterator-Funktionen.
Wie lasse ich eine Schleife in Go N-mal laufen?
Seit Go 1.22 läuft for i := range n mit i von 0 bis n-1, und for range n läuft n-mal ohne Variable. Die Form mit drei Klauseln for i := 0; i < n; i++ funktioniert in jeder Version.
Wie durchlaufe ich eine Map in sortierter Reihenfolge?
Sammle die Keys, sortiere sie und lass range dann über sie laufen. Seit Go 1.23 erledigt for _, k := range slices.Sorted(maps.Keys(m)) beides in einer Zeile. Die Iterationsreihenfolge von Maps ist absichtlich randomisiert, also verlass dich nie darauf.
Wie durchlaufe ich einen Slice in Go rückwärts?
Verwende for i := len(s) - 1; i >= 0; i-- oder seit Go 1.23 for i, v := range slices.Backward(s). Das liefert Index und Wert vom letzten bis zum ersten Element.
Warum gibt meine Goroutine in einer for-Schleife immer denselben Wert aus?
Dein Modul steht auf einer Go-Version vor 1.22, in der sich alle Iterationen die Schleifenvariable teilen. Ändere die go-Zeile in go.mod auf go 1.22 oder neuer, dann bekommt jede Iteration ihre eigene Variable. Auf einer älteren Version übergibst du den Wert als Argument (go func(h string) { ... }(host)) oder kopierst ihn mit host := host in der Schleife.
Quellen
- The Go Programming Language Specification, For statements: https://go.dev/ref/spec#For_statements
- The Go Programming Language Specification, Break statements: https://go.dev/ref/spec#Break_statements
- Effective Go, For: https://go.dev/doc/effective_go#for
- The Go Blog, Fixing For Loops in Go 1.22: https://go.dev/blog/loopvar-preview
- Go 1.22 Release Notes: https://go.dev/doc/go1.22
- Go 1.23 Release Notes: https://go.dev/doc/go1.23
- The Go Blog, Range Over Function Types: https://go.dev/blog/range-functions
- staticcheck SA4011: https://staticcheck.dev/docs/checks/#SA4011
