Andere Sprachen haben drei oder vier Schleifen-Konstrukte — Go hat genau eines: for. Diese Reduktion ist Programm. Das eine Schlüsselwort deckt durch drei Grundformen alles ab, was eine klassische while-, do-while- oder for(;;)-Schleife in C oder Java leistet, und ergänzt das mit einer eigenen range-Variante, die über Slices, Maps, Strings, Channels und seit Go 1.22 sogar Integer iteriert. Dieser Artikel arbeitet die drei Grundformen sauber heraus, zeigt jede range-Variante mit lauffähigem Beispiel, erklärt die spürbare Verhaltensänderung der Loop-Variable seit Go 1.22 und sammelt am Ende die Stolperfallen, die in Reviews regelmäßig auffallen — von der Kopie-Semantik des Range-Werts über die randomisierte Map-Iteration bis zur Composite-Literal-Falle in der Condition.
Die formale Grammatik
Die Go-Spec definiert das for-Statement minimalistisch — eine einzige Produktion deckt alle drei Formen plus range ab:
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
Condition = Expression .
PostStmt = SimpleStmt .
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .Daraus ergeben sich vier praktische Schreibweisen:
| Form | Schreibweise | Typische Verwendung |
|---|---|---|
| Klassisch | for init; cond; post { ... } | Zählschleife mit Index, C-Stil |
| While-artig | for cond { ... } | Bedingungsschleife, Polling, Suche |
| Endlos | for { ... } | Event-Loop, Server, Reader-Loop |
| range | for k, v := range x { ... } | Iteration über Container/Channels |
Vier Eigenheiten, die Go von C/Java/JavaScript unterscheiden — und die für alle vier Formen gelten:
- Keine Klammern um die Klausel.
for (i := 0; i < n; i++) { ... }ist Syntax-Fehler. Die Schlüsselwörterfor,range,break,continuebrauchen keine Parens. - Geschweifte Klammern sind Pflicht. Auch bei einer Zeile im Body.
for x { y() }ist OK,for x y()ist ein Syntax-Fehler. - Die drei Klauseln werden durch Semikolons getrennt — nur dann, wenn sie da sind.
for cond { ... }braucht keine Semikolons. - Block-Zwang macht „goto fail"-Bugs unmöglich. Ein versteckter Single-Statement-Body wie in C gibt es nicht.
Form 1 — Klassisch mit Init, Condition, Post
Die vollständige Form mit allen drei Klauseln ist der direkte Verwandte der C-for-Schleife. Init läuft einmal vor der ersten Iteration, Condition wird vor jeder Iteration geprüft, Post läuft am Ende jeder Iteration:
package main
import "fmt"
func main() {
// Standard-Zählschleife
for i := 0; i < 5; i++ {
fmt.Println("i =", i)
}
// i ist hier außerhalb nicht mehr sichtbar.
// fmt.Println(i) // Fehler: undefined: i
// Rückwärts
for j := 10; j > 0; j -= 2 {
fmt.Println("j =", j)
}
// Mehrere Variablen über Multi-Assignment in Init und Post
for a, b := 0, 1; a < 20; a, b = b, a+b {
fmt.Print(a, " ")
}
fmt.Println()
}i = 0
i = 1
i = 2
i = 3
i = 4
j = 10
j = 8
j = 6
j = 4
j = 2
0 1 1 2 3 5 8 13Drei Beobachtungen:
- Scope der Init-Variable. Wie bei
iföffnetforeinen impliziten Block — die in der Init-Klausel mit:=deklarierten Variablen leben bis zur schließenden Klammer des Bodys und sind danach weg. Das verschmutzt den Funktions-Scope nicht. - Multi-Assignment in Init und Post. Mit
a, b = b, a+bwerden beide Werte gleichzeitig getauscht — die rechte Seite wird vollständig ausgewertet, bevor irgendetwas zugewiesen wird. Ideal für Fibonacci, Pointer-Tausch, Window-Sliding. - Init und Post sind optional. Du kannst eine, zwei oder alle drei Klauseln weglassen — die Semikolons bleiben dann aber stehen, solange mindestens eine Klausel da ist.
for ; i < 10; i++ { ... }ist legal, wenniaußerhalb deklariert wurde.
Form 2 — While-artig mit nur einer Condition
Wenn nur die Condition übrig bleibt, ersetzt for die klassische while-Schleife — Go hat kein eigenes Keyword dafür:
package main
import "fmt"
func main() {
// Klassisches while-Pattern
n := 1
for n < 100 {
n *= 2
}
fmt.Println("erste Zweier-Potenz >= 100:", n)
// Suche in einem Slice
data := []int{3, 7, 1, 9, 4}
i := 0
for i < len(data) && data[i] != 9 {
i++
}
if i < len(data) {
fmt.Println("9 gefunden bei Index", i)
}
// Lesen, bis Sentinel erreicht
queue := []string{"a", "b", "STOP", "c"}
for len(queue) > 0 && queue[0] != "STOP" {
fmt.Println("verarbeite", queue[0])
queue = queue[1:]
}
}erste Zweier-Potenz >= 100: 128
9 gefunden bei Index 3
verarbeite a
verarbeite bDie while-Form ist die richtige Wahl, wenn der Loop-Index nicht der treibende Faktor ist — sondern eine externe Bedingung wie „Queue nicht leer", „Reader liefert noch Daten" oder „Convergence-Kriterium nicht erreicht".
Form 3 — Endlos mit explizitem Exit
Lässt du auch die Condition weg, läuft die Schleife unbegrenzt. Der Ausstieg passiert über break, return, panic oder os.Exit. Diese Form ist das Standard-Muster für Server-Loops, Event-Loops und Reader-Schleifen mit Sentinel:
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
func main() {
r := bufio.NewReader(strings.NewReader("erste\nzweite\ndritte\n"))
// Reader-Loop — endlos, bis EOF
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Fehler:", err)
return
}
fmt.Print("Zeile: ", line)
}
fmt.Println("fertig")
}Zeile: erste
Zeile: zweite
Zeile: dritte
fertigfor { ... } ist die idiomatische Schreibweise für „endlos". Manchmal sieht man for true { ... } — das ist legal, aber unnötig: gofmt lässt es stehen, Reviewer kürzen es zu for { ... }.
range — die idiomatische Iteration
Die range-Klausel ist Gos einheitliches Interface für Iteration über Container. Sie funktioniert über sechs Typen — Array, Slice, String, Map, Channel und (seit Go 1.22) Integer — und liefert je nach Typ einen oder zwei Werte:
| Typ | 1. Wert | 2. Wert | Besonderheit |
|---|---|---|---|
[]E / [N]E | Index (int) | Element (Kopie!) | Reihenfolge garantiert 0..len-1 |
string | Byte-Index (int) | Rune | Iteriert UTF-8-Codepoints, nicht Bytes |
map[K]V | Key | Value | Reihenfolge randomisiert |
chan E | Wert | — | Terminiert bei close(ch) |
int (Go 1.22+) | Zähler 0..n-1 | — | Kein zweiter Wert erlaubt |
| Iterator-Func (Go 1.23+) | Yield-Werte | — | Pull-basierte Iteratoren |
Die Range-Expression wird einmal vor der Schleife ausgewertet. Spätere Mutationen am ursprünglichen Slice/Map verändern den Iterationslauf nicht — du bekommst keine „live"-Sicht.
range über Slice und Array
Bei Slices und Arrays liefert range Index und einen kopierten Wert. Das ist der häufigste Anfänger-Stolperstein: wer das Element mutieren will, muss über den Index gehen — der zweite Range-Wert ist eine reine Lese-Kopie:
package main
import "fmt"
type Player struct {
Name string
Score int
}
func main() {
players := []Player{
{"Alice", 10},
{"Bob", 20},
{"Carol", 30},
}
// (1) Index + Wert (Wert ist Kopie!)
for i, p := range players {
fmt.Printf("%d: %s hat %d\n", i, p.Name, p.Score)
}
// (2) Nur Index
for i := range players {
players[i].Score += 5 // funktioniert — mutiert das Original
}
// (3) Nur Wert (Index ausblenden)
total := 0
for _, p := range players {
total += p.Score
}
fmt.Println("Summe:", total)
// (4) Falle — Mutation am Range-Wert ist sinnlos
for _, p := range players {
p.Score = 0 // p ist eine Kopie, das Original bleibt
}
fmt.Println("nach Pseudo-Reset:", players[0].Score)
}0: Alice hat 10
1: Bob hat 20
2: Carol hat 30
Summe: 75
nach Pseudo-Reset: 15Drei Lektionen:
- Wert ist Kopie. Wer Struct-Felder mutieren will, geht über den Index (
players[i].X = ...) oder verwendet ein Slice von Pointern ([]*Player). - Index ohne Wert ist die idiomatische Form, wenn der Wert nicht gebraucht wird oder mutiert werden soll.
for _, v := range arrauf einem großen Array kopiert das Array komplett, bevor iteriert wird — denn Arrays sind in Go Value-Types. Bei großen Arrays besser Pointer oder Slice nehmen.
range über Map — randomisierte Reihenfolge
Gos Map-Iteration ist absichtlich nicht deterministisch. Bei jedem Programmlauf — und sogar zwischen zwei Schleifen über dieselbe Map — kann die Reihenfolge eine andere sein. Das ist eine bewusste Design-Entscheidung, die verhindert, dass Code versehentlich auf eine implementierungsabhängige Ordnung baut:
package main
import (
"fmt"
"sort"
)
func main() {
scores := map[string]int{
"Alice": 10,
"Bob": 20,
"Carol": 30,
}
// (1) Reihenfolge ist UNBESTIMMT
for name, score := range scores {
fmt.Println(name, "->", score)
}
fmt.Println("---")
// (2) Wer sortierte Ausgabe braucht: Keys extrahieren und sortieren
keys := make([]string, 0, len(scores))
for k := range scores {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, "->", scores[k])
}
}Bob -> 20
Alice -> 10
Carol -> 30
---
Alice -> 10
Bob -> 20
Carol -> 30Auch erwähnenswert: Wer während der Iteration die Map mutiert, bewegt sich in undefiniertem Terrain. Die Spec sagt: gelöschte Einträge werden eventuell nicht mehr ausgeliefert, neu eingefügte eventuell schon. Verlass dich nicht darauf — sammle Änderungen in einem Slice und wende sie nach der Schleife an.
range über String — Runes statt Bytes
Bei Strings macht range etwas, was Anfänger oft überrascht: Es iteriert über Runes (Unicode-Codepoints), nicht über Bytes. Der erste Wert ist die Byte-Position des Codepoints, der zweite die rune selbst. Ein Multibyte-Zeichen springt entsprechend mehrere Bytes weiter:
package main
import "fmt"
func main() {
s := "Go für €"
// (1) range iteriert UTF-8-Runes
for i, r := range s {
fmt.Printf("Byte %d: rune %q (U+%04X)\n", i, r, r)
}
fmt.Println("Byte-Länge:", len(s))
// (2) Klassische for-Schleife iteriert Bytes
fmt.Println("--- Byte-Iteration ---")
for i := 0; i < len(s); i++ {
fmt.Printf("Byte %d: %d\n", i, s[i])
}
}Byte 0: rune 'G' (U+0047)
Byte 1: rune 'o' (U+006F)
Byte 2: rune ' ' (U+0020)
Byte 3: rune 'f' (U+0066)
Byte 4: rune 'ü' (U+00FC)
Byte 6: rune 'r' (U+0072)
Byte 7: rune ' ' (U+0020)
Byte 8: rune '€' (U+20AC)
Byte-Länge: 11Zwei Punkte zum Mitnehmen:
- Die Index-Sprünge sind nicht 0, 1, 2, ..., sondern nach Byte-Breite des vorherigen Codepoints.
übelegt 2 Bytes,€belegt 3 Bytes — danach springt der Index entsprechend weiter. - Bei ungültigem UTF-8 liefert
rangedie Replacement-RuneU+FFFDund schiebt den Index um genau 1 Byte weiter. So bleibt die Iteration auch bei kaputten Daten endlich.
Wer wirklich byteweise iterieren will (etwa für reine ASCII-Verarbeitung oder Performance-Pfade), nimmt die klassische for i := 0; i < len(s); i++-Form mit s[i] — das liefert byte, nicht rune.
range über Channel — terminiert bei close
Bei Channels liefert range einen einzelnen Wert pro Iteration — den Wert, der aus dem Channel empfangen wurde. Die Schleife terminiert, sobald der Channel geschlossen und leer ist:
package main
import "fmt"
func produce(ch chan<- int) {
defer close(ch) // close beendet den Range-Loop beim Empfänger
for i := 1; i <= 5; i++ {
ch <- i * i
}
}
func main() {
ch := make(chan int)
go produce(ch)
// range terminiert automatisch nach close
for v := range ch {
fmt.Println("empfangen:", v)
}
fmt.Println("Channel geschlossen, Loop beendet")
}empfangen: 1
empfangen: 4
empfangen: 9
empfangen: 16
empfangen: 25
fertig
Channel geschlossen, Loop beendetDrei Anmerkungen:
- Wer den Channel nicht schließt, blockiert die Range-Schleife für immer (Deadlock, sobald keine Sender mehr da sind). Schließen ist Sender-Verantwortung — niemals der Empfänger.
- Nil-Channel in
range: Empfangen aus einemnil-Channel blockiert ewig — beirangeheißt das: die Schleife wird nie etwas tun und nie zurückkehren. - Nur ein Range-Wert. Im Gegensatz zum direkten
v, ok := <-chbekommst du in der Range-Form keinok— der Loop kümmert sich um die Terminierung selbst.
range über Integer (Go 1.22+) und Funktion (Go 1.23+)
Seit Go 1.22 darf der Range-Ausdruck auch ein Integer sein. Das ist Zucker für die häufige for i := 0; i < n; i++-Form, aber ohne die ergonomische Last:
package main
import "fmt"
func main() {
// n-mal — der idiomatische Ersatz für for i := 0; i < n; i++
for i := range 5 {
fmt.Println("i =", i)
}
// Wer den Zähler nicht braucht: range ohne linke Seite
count := 0
for range 3 {
count++
}
fmt.Println("count =", count)
// Negative oder Null: keine Iteration
for i := range 0 {
fmt.Println("nicht erreicht", i)
}
fmt.Println("nach for range 0")
}i = 0
i = 1
i = 2
i = 3
i = 4
count = 3
nach for range 0Eine wichtige Einschränkung: Bei range n ist nur ein Range-Wert erlaubt (der Zähler). Ein zweiter Wert wäre ein Syntax-Fehler — anders als bei Slice/Map/String.
Funktions-Iteratoren (Go 1.23+). Mit Go 1.23 kommt range über Funktionen — die Basis für Pull-basierte Iteratoren in der Standard-Library (siehe iter-Paket). Eine kurze Skizze:
package main
import "fmt"
// Iterator-Funktion: liefert Werte an einen Yield-Callback
func upTo(n int) func(yield func(int) bool) {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return // Konsument hat break gemacht
}
}
}
}
func main() {
for v := range upTo(4) {
fmt.Println("v =", v)
}
}v = 0
v = 1
v = 2
v = 3Details zu Iteratoren gehören in einen eigenen Artikel — hier reicht: Es gibt sie, sie sind das Fundament für iter.Seq / iter.Seq2 und neue Library-APIs wie maps.Keys, slices.All.
Die Loop-Variable seit Go 1.22 — die wichtigste Verhaltensänderung
Bis Go 1.21 war die Loop-Variable eine einzige Variable für alle Iterationen — alle Closures und Goroutinen, die sie einfingen, teilten sich denselben Speicherplatz. Das war die häufigste Bug-Quelle in echtem Go-Code. Seit Go 1.22 bekommt jede Iteration ihre eigene Variable:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
values := []string{"a", "b", "c"}
for _, v := range values {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v)
}()
}
wg.Wait()
}# Go 1.22+ (module declares go 1.22 oder höher):
a
b
c
# in irgendeiner Reihenfolge
# Go 1.21 oder älter (oder module declares go 1.21):
c
c
cDie Änderung greift nur, wenn das Modul in go.mod go 1.22 oder höher deklariert. Älterer Code läuft unverändert weiter — das war Voraussetzung für die Aufnahme dieser Verhaltensänderung in eine stabile Sprache.
Vor Go 1.22 — der manuelle Workaround. Wer das alte Verhalten umgehen wollte, deklarierte die Variable im Body neu — Shadowing als Feature:
// Pre-1.22-Workaround Variante A: Shadow im Body
for _, v := range values {
v := v // neue v-Variable pro Iteration
go func() { fmt.Println(v) }()
}
// Pre-1.22-Workaround Variante B: Wert als Parameter
for _, v := range values {
go func(v string) { fmt.Println(v) }(v)
}Diese Patterns funktionieren weiterhin und schaden auch unter Go 1.22+ nicht — sie sind nur schlicht nicht mehr nötig. In neu geschriebenem Code mit go 1.22+ kannst du die Closure direkt schreiben. Detail aus dem Blog-Post: Das Team hat 2023 mit GOEXPERIMENT=loopvar einen Vorlauf gemacht, in dem große Codebases (inkl. Kubernetes) das neue Verhalten geprüft haben — Bugs durch das alte Verhalten waren häufig, durch das neue extrem selten.
break, continue, Labels
break verlässt die innerste umgebende Schleife oder den Switch. continue springt zur Post-Klausel (klassische Form) bzw. zur nächsten Iteration. Wer aus mehreren verschachtelten Schleifen ausbrechen will, nutzt Labels:
package main
import "fmt"
func main() {
grid := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
outer:
for i, row := range grid {
for j, v := range row {
if v == 5 {
fmt.Printf("gefunden bei (%d, %d)\n", i, j)
break outer // bricht beide Schleifen ab
}
}
}
// continue mit Filter
for i := 1; i <= 10; i++ {
if i%2 == 0 {
continue
}
fmt.Print(i, " ")
}
fmt.Println()
}gefunden bei (1, 1)
1 3 5 7 9 Mehr Details — zum Beispiel die Unterschiede zwischen break, continue, goto und return aus einer Schleife heraus — stehen im eigenen Artikel zu break, continue, goto.
Häufige Stolperfallen
Loop-Variable in Closure / Goroutine — pre-1.22 die Bug-Klasse Nr. 1.
Vor Go 1.22 teilten sich alle Iterationen eine einzige Variable. for _, v := range xs { go func() { use(v) }() } rief am Ende oft use(letztesElement) dreimal auf — nicht das, was du wolltest. Seit Go 1.22 hat jede Iteration ihre eigene v. Prüfe deine go.mod: go 1.22 oder höher aktiviert das neue Verhalten.
Der zweite range-Wert ist eine Kopie, kein Pointer.
for _, p := range players { p.Score++ } mutiert nichts — p ist eine Kopie der Struct. Wer das Original ändern will, geht entweder über den Index (players[i].Score++) oder iteriert über ein Slice von Pointern ([]*Player). Auch Memory-bewusst: bei großen Structs kann die Kopie teuer sein.
Map-Iteration ist absichtlich randomisiert.
Die Reihenfolge ist nicht nur „nicht garantiert", sondern aktiv zufallsbedingt — Go würfelt den Start-Slot. Wer deterministische Ausgabe braucht (Logs, Tests, JSON-Snapshots), sammelt die Keys, sortiert sie, und iteriert dann über das sortierte Slice.
range über String iteriert Runes, nicht Bytes.
Bei UTF-8-Inhalten springt der Index in Schritten der Codepoint-Bytebreite — 1, 2, 3 oder 4 Bytes. Wer byteweise verarbeiten muss, nimmt for i := 0; i < len(s); i++ { ... s[i] ... }. Ungültiges UTF-8 erzeugt U+FFFD und schiebt um 1 Byte weiter.
range über Channel terminiert nie ohne close.
for v := range ch läuft, bis der Channel geschlossen ist. Wenn niemand close(ch) ruft, blockiert der Empfänger bei leerem Channel — Deadlock-Risiko. close ist immer Aufgabe des Senders, niemals des Empfängers, und nur exakt einmal pro Channel zulässig.
Endlos-Schleife ohne sicheren Exit-Pfad.
for { ... } ohne break, return, panic oder os.Exit ist ein Hang-Bug. Besonders heimtückisch: ein break in einem select innerhalb des Loops bricht nur das select ab, nicht den for. Wer aus dem for heraus will, braucht ein Label: for { select { case ...: break outer } }.
Composite-Literal-Ambiguität in der Condition.
for p := next(); p == Point{0, 0}; p = next() { ... } ist Syntax-Fehler — der Compiler liest Point{ als Body-Anfang. Lösung: Klammern um das Literal (p == (Point{0, 0})) oder eine vorgeschobene Variable. Dieselbe Falle hatte schon if und switch.
Off-by-one — < vs. <=.
for i := 0; i <= len(s); i++ { s[i] } greift einen Index zu weit zu und löst eine Panic aus. Idiomatisch ist i < len(s), weil Indizes 0-basiert sind und der letzte gültige Index len-1 ist. Bei Range-Loops umgehst du das Problem komplett — range läuft genau über die gültigen Indizes.
Range-Expression wird einmal ausgewertet.
for i, v := range slice { slice = nil } läuft trotz slice = nil zu Ende — die Schleife arbeitet mit der ursprünglichen Slice-Header-Kopie. Auch len() in der klassischen Form (for i := 0; i < len(s); i++) ist OK: len ist O(1), und bei stabiler Länge gibt es keinen Effizienz- oder Korrektheits-Verlust.
Weiterführende Ressourcen
Externe Quellen
- For statements – Go Language Specification
- For statements with range clause – Go Specification
- Fixing For Loops in Go 1.22 – Go Blog
- Effective Go: For
iter-Paket — Range over Function Types (Go 1.23+)
Verwandte Artikel
- if – Bedingungen mit lokalem Scope
- switch – die strukturierte Fallunterscheidung
- defer – aufgeschobene Ausführung
- break, continue, goto – Sprünge in Schleifen
- Scoping-Regeln – die fünf Ebenen und die for-Block-Mechanik
- Slice – Aufbau, Header, Kopier-Semantik
- Map – Hash-Tabelle mit randomisierter Reihenfolge
- String – Bytes, Runes und UTF-8
- Rune – Codepoint statt Byte