Vier Sprünge regeln in Go den Kontrollfluss innerhalb von Schleifen, switch und select: break verlässt das umschließende Konstrukt, continue springt zur nächsten Iteration einer for-Schleife, Labels geben einer Loop oder einem switch/select einen Namen, damit break und continue gezielt darauf zielen können, und goto springt innerhalb einer Funktion zu einer markierten Stelle. Drei davon braucht jeder Go-Programmierer täglich, das vierte — goto — ist legal, aber so selten sinnvoll, dass viele erfahrene Gopher es noch nie geschrieben haben. Dieser Artikel arbeitet alle vier formal durch, zeigt die typischen Muster (2D-Suche, „break aus switch in for"), die harten Regeln (goto darf nicht über Variablen-Deklarationen springen) und die Stolperfallen, an denen Anfänger und Fortgeschrittene gleichermaßen scheitern.

break — das innerste Konstrukt verlassen

break beendet die Ausführung des innersten umschließenden for, switch oder select. Die Spec definiert die Grammatik knapp:

EBNF BreakStmt (Go-Spec)
BreakStmt = "break" [ Label ] .

In der Grundform — ohne Label — verlässt break genau das eine Konstrukt, in dem es direkt steht. Alles, was außenrum noch läuft, bleibt unberührt:

Go break_basic.go
package main

import "fmt"

func main() {
    // (1) break in einer for-Schleife
    for i := 0; i < 10; i++ {
        if i == 3 {
            break
        }
        fmt.Println("i =", i)
    }

    // (2) break in einem switch — beendet nur den case-Body.
    // Da Go switch-cases nicht automatisch fallthroughen,
    // ist break hier praktisch nie nötig — der case endet sowieso.
    switch x := 2; x {
    case 1:
        fmt.Println("eins")
    case 2:
        fmt.Println("zwei")
        break // redundant, aber legal
    }

    // (3) break in select — verlässt das select, nicht eine umgebende Schleife.
    ch := make(chan int, 1)
    ch <- 42
    select {
    case v := <-ch:
        fmt.Println("empfangen:", v)
        break
    }
}
Output
i = 0
i = 1
i = 2
zwei
empfangen: 42

Zwei Beobachtungen, die später wichtig werden:

  • In switch ist break redundant, weil Go-Cases nicht durchfallen — der Block endet implizit am nächsten case. Wer aus C kommt, schreibt break aus Gewohnheit; es schadet nicht, ist aber Rauschen.
  • In select beendet break ausschließlich das select. Steht das select wiederum in einer for-Schleife, läuft die Schleife unbeeindruckt weiter — eine sehr häufige Falle bei Channel-Code.

continue — nächste Iteration der innersten Schleife

continue ist breaks leiserer Verwandter: Es überspringt den Rest des aktuellen Schleifen-Durchlaufs und springt zur Post-Statement-Auswertung (bzw. zum nächsten Range-Element) der innersten umschließenden for-Schleife. Auch hier nimmt die Spec optional ein Label entgegen:

EBNF ContinueStmt (Go-Spec)
ContinueStmt = "continue" [ Label ] .

Anders als break funktioniert continue nur in for — in switch oder select ist continue ein Compile-Fehler, sofern keine äußere for-Schleife im Spiel ist:

Go continue_basic.go
package main

import "fmt"

func main() {
    // Nur ungerade Zahlen ausgeben
    for i := 0; i < 6; i++ {
        if i%2 == 0 {
            continue
        }
        fmt.Println(i)
    }

    // Range-Variante: Leere Strings überspringen
    words := []string{"alpha", "", "beta", "", "gamma"}
    for idx, w := range words {
        if w == "" {
            continue
        }
        fmt.Printf("[%d] %s\n", idx, w)
    }
}
Output
1
3
5
[0] alpha
[2] beta
[4] gamma

Der Effekt ist exakt: Post-Statement (i++ bzw. die Range-Iteration) läuft, die Condition wird neu geprüft, der nächste Durchlauf beginnt. In einer for { ... }-Endlosschleife springt continue direkt zurück an den Schleifen-Anfang — kein Post-Statement, keine Condition, einfach erneut.

Labels — Namen für Schleifen, switch und select

Ein Label ist ein Identifier, gefolgt von Doppelpunkt, der direkt vor einer for-, switch- oder select-Anweisung steht. Er gibt dem Konstrukt einen Namen, auf den break, continue und goto zielen können:

EBNF LabeledStmt (Go-Spec)
LabeledStmt = Label ":" Statement .
Label       = identifier .

Drei Regeln zum Geltungsbereich:

  1. Funktions-Scope. Labels leben im Scope der umschließenden Funktion, nicht im Block. Du kannst aus einem inneren Block heraus auf ein äußeres Label zielen, aber niemals über Funktionsgrenzen springen.
  2. Eindeutig pro Funktion. Zwei Labels mit demselben Namen in derselben Funktion sind ein Compile-Fehler.
  3. Nicht-benutzte Labels sind ein Fehler. Genau wie ungenutzte Variablen — der Compiler bricht ab. goto-Targets bilden hier den einzigen Ausnahmebereich, aber auch sie müssen erreichbar sein.

Naming-Konvention. Die Standard-Library schreibt Labels mit einem Großbuchstaben am Anfang (Outer, NextRow, Loop). Das ist keine Export-Markierung — Labels sind ohnehin funktions-lokal. Es ist reine Konvention, damit Labels visuell aus dem Code herausstehen. Beschreibend wählen: Outer ist OK, besser sind sprechende Namen wie nextRow, scanResult, done.

Go label_syntax.go
package main

func main() {
    // (1) Label vor einer for-Schleife
    Outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i*j > 2 {
                break Outer
            }
        }
    }

    // (2) Label vor einem switch
    Dispatch:
    switch x := 5; {
    case x > 0:
        break Dispatch // identisch zu plain break — Label ist hier sinnlos
    }

    // (3) Label vor select
    Wait:
    for {
        select {
        case <-done():
            break Wait // verlässt die for, nicht nur das select
        }
    }
}

func done() <-chan struct{} { c := make(chan struct{}); close(c); return c }

Die Klausel „direkt davor" ist strikt: Zwischen Label: und dem Konstrukt darf nichts stehen — kein Kommentar in eigener Zeile (formal ja, praktisch aber unsauber), keine Leeranweisung, keine andere Statement. gofmt formatiert Labels typischerweise eine Einrückungs-Stufe links vom Code, damit sie als Marker hervortreten.

break Label — eine äußere Schleife gezielt verlassen

Der klassische Anwendungsfall: zwei verschachtelte Schleifen, die innere findet etwas, und du willst beide auf einmal verlassen. Ohne Label müsste man entweder ein Flag setzen oder ein Refactoring in eine eigene Funktion mit return machen. Mit Label genügt ein einziger Sprung:

Go break_label_2d_search.go
package main

import "fmt"

func main() {
    grid := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    target := 5

    var foundRow, foundCol int = -1, -1

Search:
    for row, line := range grid {
        for col, v := range line {
            if v == target {
                foundRow, foundCol = row, col
                break Search
            }
        }
    }

    if foundRow >= 0 {
        fmt.Printf("Treffer: grid[%d][%d] = %d\n", foundRow, foundCol, target)
    } else {
        fmt.Println("nicht gefunden")
    }
}
Output
Treffer: grid[1][1] = 5

break Search verlässt beide Schleifen auf einmal — der Kontrollfluss landet bei der nächsten Anweisung nach der äußeren for. Ohne Label würde nur die innere Schleife enden, und die äußere würde noch eine Zeile lang weiterprüfen.

Wann ist die Refactoring-Alternative besser? Wenn die Suche eine eigenständige Operation ist und ein Ergebnis liefert, ist eine Hilfsfunktion mit return oft sauberer:

Go break_label_vs_return.go
// Variante A: break Label — Treffer wird über äußere Variablen festgehalten
// siehe oben

// Variante B: Refactor in eigene Funktion mit return
func find(grid [][]int, target int) (row, col int, ok bool) {
    for r, line := range grid {
        for c, v := range line {
            if v == target {
                return r, c, true
            }
        }
    }
    return -1, -1, false
}

Variante B ist meistens vorzuziehen, sobald die Suche zweimal vorkommt oder die innere Logik wächst. break Label bleibt sinnvoll, wenn die innere Schleife auf lokale Zustände des Aufrufers zugreift, die als Parameter umständlich wären.

continue Label — zur äußeren Iteration springen

Seltener als break Label, aber gelegentlich genau richtig: Du willst aus einer inneren Schleife heraus die nächste Iteration der äußeren Schleife auslösen. Klassisches Beispiel — Validierung pro Zeile:

Go continue_label.go
package main

import "fmt"

func main() {
    records := [][]string{
        {"alice", "30", "berlin"},
        {"bob", "", "hamburg"},   // unvollständig
        {"carol", "27", ""},      // unvollständig
        {"dave", "41", "munich"},
    }

NextRecord:
    for i, rec := range records {
        for j, field := range rec {
            if field == "" {
                fmt.Printf("record %d: feld %d leer — überspringe\n", i, j)
                continue NextRecord
            }
        }
        fmt.Printf("record %d ok: %v\n", i, rec)
    }
}
Output
record 0 ok: [alice 30 berlin]
record 1: feld 1 leer — überspringe
record 2: feld 2 leer — überspringe
record 3 ok: [dave 41 munich]

continue NextRecord springt direkt zum Range-Schritt der äußeren Schleife — die innere wird komplett abgebrochen, der „record ok"-Print weiter unten wird übersprungen. Ohne Label müsstest du ein Flag setzen, das nach der inneren Schleife wieder geprüft wird, oder die innere Schleife in eine Funktion auslagern.

break in switch innerhalb von for — die häufigste Verwechslung

Der subtile Klassiker: Ein switch (oder select) liegt in einer for-Schleife, und du willst aus der Schleife ausbrechen. Naive Schreibweise:

Go break_switch_trap.go
package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        switch {
        case i == 2:
            fmt.Println("treffer, will raus aus der for")
            break // bricht NUR das switch — die for läuft weiter
        default:
            fmt.Println("i =", i)
        }
    }
    fmt.Println("nach der for")
}
Output
i = 0
i = 1
treffer, will raus aus der for
i = 3
i = 4
nach der for

Das break zielt — laut Spec — auf das innerste umschließende for/switch/select. Das ist hier das switch. Die for läuft fröhlich weiter. Die Lösung: Label vor die for setzen und gezielt break Label schreiben.

Go break_switch_label.go
package main

import "fmt"

func main() {
Loop:
    for i := 0; i < 5; i++ {
        switch {
        case i == 2:
            fmt.Println("treffer, raus aus der for")
            break Loop
        default:
            fmt.Println("i =", i)
        }
    }
    fmt.Println("nach der for")
}
Output
i = 0
i = 1
treffer, raus aus der for
nach der for

Exakt dieselbe Falle gilt für select: break ohne Label verlässt nur das select. Wer aus einer Channel-Loop ausbrechen will, muss das umschließende for labeln.

goto — erlaubt, aber mit harten Regeln

Go hat goto. Die Spec lässt es zu, weil es in Code-Generatoren und in einer Handvoll Algorithmus-Mustern (zustandsmaschinen-artige Cleanup-Sequenzen) die klarste Lösung ist. Im gewöhnlichen Anwendungscode taucht es praktisch nie auf.

EBNF GotoStmt (Go-Spec)
GotoStmt = "goto" Label .

Zwei Regeln machen goto deutlich harmloser als sein C-Vorbild:

  1. Kein Sprung über Variablen-Deklarationen. Wenn zwischen dem goto und seinem Ziel eine Variable mit := oder var deklariert wird, die am Ziel noch im Scope ist, lehnt der Compiler ab: „goto X jumps over declaration of Y". Damit ist ausgeschlossen, dass goto versehentlich eine undefinierte Variable nutzt.
  2. Kein Sprung in einen fremden Block hinein. Du darfst aus einem inneren Block per goto nach außen springen, aber nicht in einen Block hinein, in dem das goto nicht selbst steht.
Go goto_legal.go
package main

import "fmt"

func main() {
    i := 0

Again:
    if i < 3 {
        fmt.Println("i =", i)
        i++
        goto Again // legal — kein Variablen-Decl im Pfad
    }

    fmt.Println("fertig")
}
Output
i = 0
i = 1
i = 2
fertig

Beide Verbote in Aktion — was der Compiler ablehnt:

Go goto_illegal.go
package main

func badJumpOverDecl() {
    goto End // FEHLER: goto End jumps over declaration of x
    x := 42
    _ = x
End:
    return
}

func badJumpIntoBlock() {
    goto Inside // FEHLER: goto Inside jumps into block starting at ...
    {
    Inside:
        println("nicht erreichbar")
    }
}

Sinnvolle Use-Cases — die Liste ist kurz:

  • Generierter Code. Tools wie protoc-gen-go oder Parser-Generatoren produzieren Zustandsmaschinen, in denen goto zwischen Zuständen die natürlichste Darstellung ist.
  • Error-Cleanup mit gemeinsamer Aufräumsequenz. Selten, weil defer denselben Job in fast allen Fällen besser macht. Die Standard-Library hat ein paar historische Stellen, aber neue Codebases brauchen das nicht.
  • Hot Loops mit messbarem Profit. Theoretisch könnte ein expliziter goto einem Compiler helfen, aber die Go-Compiler-Optimierungen machen das praktisch obsolet.

Faustregel: Wenn du goto schreiben willst, frag dich erst, ob for, break Label, continue Label oder ein Refactoring in eine Funktion nicht besser wäre. In den meisten Fällen lautet die Antwort ja.

Anti-Patterns

Ein paar Muster, die in Code-Reviews regelmäßig rotmarkiert werden:

Go anti_patterns.go
// (1) goto für gewöhnliche Schleifen-Logik
// schlecht:
func badLoop() {
    i := 0
Loop:
    if i < 10 {
        println(i)
        i++
        goto Loop
    }
}
// besser: for i := 0; i < 10; i++ { println(i) }

// (2) Label auf einem switch, das gar nicht referenziert wird
// Labels nur setzen, wenn jemand sie tatsächlich anspringt.

// (3) break Label, wo eine Hilfsfunktion mit return klarer wäre
// Wenn die innere Schleife eine in sich geschlossene Operation
// (z. B. eine Suche) ist, extrahiere sie und nutze return.

// (4) Generische Label-Namen wie L1, L2, X, Y
// Labels sind Markierungen für menschliche Leser — beschreibend
// benennen: Outer, NextRecord, Done, Drain.

// (5) break in switch, das gar nichts überspringen soll
// In Go fallen cases nicht durch — break am Ende eines case ist
// immer redundant. Weglassen.

Das übergreifende Prinzip: Sprünge sind ein Werkzeug, das die lineare Lesbarkeit unterbricht. Jede Verwendung muss sich rechtfertigen — entweder durch klare Verbesserung der Lesbarkeit (klassisch break Label bei 2D-Suche) oder durch echte Notwendigkeit (generierter Code). Im Zweifel: Refactoring in eine Funktion schlägt fast immer.

Zusammenspiel mit defer und panic

Eine letzte Eigenheit, die regelmäßig für Verwirrung sorgt: break, continue und goto sind gewöhnliche Kontrollfluss-Statements — sie lösen defer-Aufrufe aus, sofern sie eine Funktion verlassen würden, aber innerhalb derselben Funktion bleibt der Stack stabil.

Go break_defer.go
package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        defer fmt.Println("defer im Loop:", i)
        if i == 1 {
            break
        }
    }
    fmt.Println("nach der for")
    // defer feuert erst beim main-Return — also nach „nach der for"
}
Output
nach der for
defer im Loop: 1
defer im Loop: 0

Pointe: defer ist funktions-scoped, nicht block-scoped. break (mit oder ohne Label) löst keine defers aus, weil keine Funktion verlassen wird. Wer pro Iteration aufräumen will, packt den defer in eine eigene Hilfsfunktion oder verwendet eine IIFE-ähnliche func() { defer ... }()-Konstruktion. Details dazu im Defer-Artikel.

Häufige Stolperfallen

break in switch innerhalb einer for bricht nur das switch.

Die häufigste Verwechslung. break zielt immer auf das innerste umschließende for/switch/select. Wer aus der for ausbrechen will, muss die for mit einem Label markieren und break Loop schreiben. Gleiche Falle bei select in for.

continue funktioniert nur in for — nicht in switch oder select.

continue ohne umschließende for ist ein Compile-Fehler. In einer for-Schleife mit innerem switch zielt continue auf die for, nicht auf das switch — das ist die Ausnahme zur „innerstes Konstrukt"-Regel, weil switch und select schlicht keine Iteration haben.

Labels müssen direkt vor einer Loop, einem switch oder select stehen.

Foo: gefolgt von einem if oder x := 1 ist legal als labeled statement, aber break Foo darauf ist sinnlos — break braucht ein Loop-/Switch-/Select-Ziel. continue Foo verlangt zwingend ein for direkt hinter dem Label. Falsch platzierte Labels schlagen mit invalid break label fehl.

goto kann nicht über Variablen-Deklarationen springen.

Wenn zwischen goto X und dem Label X: ein var oder := liegt, lehnt der Compiler ab: „goto X jumps over declaration of Y". Das schützt vor undefiniertem Zugriff am Ziel. Workaround: Variable vorher deklarieren, am Ziel nur zuweisen.

goto kann nicht in einen fremden Block hineinspringen.

Du darfst aus einem inneren Block heraus nach außen springen, aber niemals von außen in einen verschachtelten Block hinein. goto Inside, wenn Inside: in einem Sub-Block sitzt, in dem das goto nicht selbst steht, ist ein Compile-Fehler. Damit ist „Sprung in eine Schleife mitten hinein" generell unmöglich.

Label-Name verkürzt nichts — vollständig beschreibend wählen.

L, L1, X sind technisch erlaubt, aber semantisch leer. Lesbare Labels heißen Outer, NextRecord, Drain, Done — sie sagen, warum gesprungen wird. Da Labels selten sind, fällt jeder im Code auf, und ein guter Name erklärt den Sprung sofort.

break ohne Label in select beendet nur das select.

Steht das select in einer äußeren for, läuft die for nach dem break weiter. In Channel-Loops, in denen ein bestimmtes Ereignis das Ganze beenden soll (done-Channel-Pattern), muss die for ein Label tragen und das break Label darauf zielen — sonst dreht die Schleife endlos weiter.

Labels sind nicht der C-Style „computed goto“.

Go-Labels werden zur Compile-Zeit aufgelöst — du kannst nicht zur Laufzeit entscheiden, wohin gesprungen wird. Jedes break, continue oder goto verweist auf genau ein statisch bekanntes Ziel. Dynamische Dispatch-Tabellen baut man in Go mit Funktions-Maps oder switch, nicht mit Labels.

Label-Naming-Konvention: erster Buchstabe groß — aber kein Export.

Die Standard-Library schreibt Outer:, Loop: mit Großbuchstaben. Das ist reine Lesbarkeits-Konvention — Labels sind funktions-lokal und kennen keine Exportierung. Mit Kleinbuchstaben funktioniert es identisch; Großschreibung hebt Labels visuell vom umgebenden Code ab.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Kontrollstrukturen

Zur Übersicht