Zurück zum Blog

Das Go-Keyword for: Schleifen, range, Schleifenvariablen und Labels

So funktioniert das Go-Keyword for: die drei Schleifenformen, for range über Slices, Maps, Strings, Channels, Integer und Iteratoren, Schleifenvariablen seit Go 1.22, break und continue mit Labels und häufige Fehler in Schleifen.

Das Go-Keyword for: Schleifen, range, Schleifenvariablen und Labels

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, und for { ... } läuft, bis ein break, return oder panic sie beendet.
  • for i, v := range x iteriert ü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.mod go 1.22 oder 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 break ohne Label innerhalb eines select oder switch verlä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.gogo
package 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.texttext
attempt 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.gogo
	for (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.gogo
package 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.texttext
processing 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.gogo
	for {
		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 überErster WertZweiter Wert
Slice oder ArrayIndexKopie des Elements
StringByte-OffsetRune (Unicode-Codepoint)
MapKeyValue
Channelempfangener Wertkeiner
Integer n (Go 1.22)0 bis n-1keiner
Iterator-Funktion (Go 1.23)was sie liefertwas 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.gogo
package 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.texttext
0 /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.gogo
package 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.texttext
main.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.gogo
package 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.texttext
false 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.gogo
	var 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.texttext
4200 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.gogo
package 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.texttext
file 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.gogo
func 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.texttext
main.go:20:2: unreachable code

und staticcheck meldet das break selbst:

example.texttext
main.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.gogo
func 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.texttext
handled 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.gogo
	for _, 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.gogo
	urls := []string{"/", "/about"}
	for _, u := range urls {
		urls = append(urls, u+"/sitemap.xml")
	}
	fmt.Println(len(urls), urls)
example.texttext
4 [/ /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.gogo
	for 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.gogo
	cursor := ""
	for {
		page := fetchPage(cursor)
		fmt.Println("fetched", page.Items)
		cursor = page.Next
		if cursor == "" {
			break
		}
	}
example.texttext
fetched [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

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen