Go kennt zwei Formen des if-Statements. Die einfache, die jede Sprache hat — und die idiomatische mit Init-Klausel, in der du vor der Bedingung noch eine Variable einführen darfst, deren Sichtbarkeit auf die Verzweigung beschränkt bleibt. Diese zweite Form ist nicht Kosmetik: Sie ist der wichtigste Baustein für comma-ok-Checks, kompaktes Error-Handling und das typische Guard-Clause-Muster. Wer sie sauber einsetzt, schreibt linearen, gut lesbaren Code und vermeidet die häufigste Falle des Walrus-Operators: das versehentliche Shadowing. Dieser Artikel arbeitet beide Formen formal durch, zeigt das Scoping in allen Verzweigungs-Zweigen und kompiliert die Stolperfallen, die in Codebases regelmäßig zuschlagen.

Die zwei Formen des if-Statements

Die Go-Spec definiert das if-Statement in der Kurzform:

EBNF IfStmt (Go-Spec)
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .

In Worten: vor der Bedingung darf — optional — ein SimpleStatement stehen, abgetrennt durch ein Semikolon. Das ist die Init-Klausel. Sie kann eine kurze Variablendeklaration (:=), eine Zuweisung (=), ein Funktionsaufruf oder ein Inkrement (x++) sein — alles, was kein Block-Statement ist.

Go if_forms.go
package main

import "fmt"

func main() {
    n := 7

    // (1) Einfache Form — Condition allein
    if n > 5 {
        fmt.Println("groß")
    }

    // (2) Init-Form — Statement; Condition
    if double := n * 2; double > 10 {
        fmt.Println("doppelt groß:", double)
    }

    // double ist hier außerhalb nicht mehr sichtbar.
    // fmt.Println(double)  // Fehler: undefined: double

    // (3) else / else if mit derselben Init-Variable
    if r := n % 2; r == 0 {
        fmt.Println("gerade")
    } else if r == 1 {
        fmt.Println("ungerade, Rest:", r)
    } else {
        fmt.Println("unmöglich")
    }
}
Output
groß
doppelt groß: 14
ungerade, Rest: 1

Drei Eigenheiten, die Go von C/Java/JavaScript unterscheiden:

  • Keine Klammern um die Condition. if (n > 5) { ... } ist erlaubt, aber unidiomatisch — gofmt lässt sie stehen, der Style-Guide rät davon ab.
  • Geschweifte Klammern sind Pflicht. Auch bei einer Zeile im Body. if x { y() } ist OK, if x y() ist Syntax-Fehler.
  • Init und Condition sind durch Semikolon getrennt, nicht durch Komma. Das Semikolon ist Pflicht, wenn eine Init-Klausel da ist.

Was in die Init-Klausel darf

Ein SimpleStatement umfasst laut Spec: Empty-Statement, Expression-Statement (Funktionsaufruf), Send-Statement (Channel), IncDecStmt (x++, x--), Assignment (=), Short-Variable-Declaration (:=). Nicht erlaubt sind Blockstatements, weitere if/for/switch, return, defer, go, fallthrough, break, continue, goto, labeled statements.

Go if_init_forms.go
package main

import (
    "fmt"
    "strconv"
)

var counter int

func main() {
    // (a) Short-Variable-Decl — der häufigste Fall
    if n, err := strconv.Atoi("42"); err == nil {
        fmt.Println("Zahl:", n)
    }

    // (b) Reine Zuweisung (existierende Variable)
    var msg string
    if msg = greet(); msg != "" {
        fmt.Println(msg)
    }

    // (c) Funktionsaufruf ohne Variablen-Bindung
    if println("Tick"); counter < 10 {
        counter++
    }

    // (d) Inkrement
    if counter++; counter > 5 {
        fmt.Println("über 5:", counter)
    }
}

func greet() string { return "hallo" }

(a) ist die typische Form. (b) sieht man, wenn die Variable außerhalb weiterleben muss. (c) und (d) sind seltene Spezialfälle — meistens schreibt man das Statement davor, weil es lesbarer ist.

Die Reihenfolge ist strikt: zuerst läuft die Init-Klausel, dann wird die Condition ausgewertet, dann der Body. Wenn die Condition false ist, wird der Body übersprungen — aber die Init-Klausel ist trotzdem schon gelaufen. Das ist relevant bei seiten-effektigen Aufrufen.

Scoping — der entscheidende Punkt

Die Spec sagt knapp:

Each if, for, and switch statement is considered to be in its own implicit block.

Praktisch heißt das: Eine in der Init-Klausel deklarierte Variable ist sichtbar in der gesamten if-else if-else-Kette — und endet mit der schließenden Klammer der letzten Verzweigung. Außerhalb ist sie weg.

Go if_scope.go
package main

import "fmt"

func main() {
    if x := 10; x > 5 {
        fmt.Println("groß, x =", x) // sichtbar
    } else if x < 0 {
        fmt.Println("negativ, x =", x) // immer noch sichtbar
    } else {
        fmt.Println("klein, x =", x) // auch hier
    }

    // x ist hier nicht mehr im Scope.
    // fmt.Println(x)  // Fehler: undefined: x
}
Output
groß, x = 10

Das ist eine sehr saubere Scope-Geometrie: Die Variable lebt genau dort, wo sie semantisch hingehört — im Block, der ihren Wert braucht — und verschmutzt den umgebenden Funktions-Scope nicht. Das ist auch der Grund, warum die Init-Form bei Code-Reviewern besonders gut ankommt.

Wichtig: Der Body eines else-Zweigs ist ein separater Block, der im impliziten Block des if verschachtelt ist. Du kannst dort eigene Variablen mit dem gleichen Namen deklarieren — und shadowst dann die aus der Init-Klausel:

Go if_else_shadow.go
package main

import "fmt"

func main() {
    if x := 10; x > 5 {
        fmt.Println("if-Body, x =", x)
    } else {
        x := -1 // NEUE Variable — shadowt die äußere x
        fmt.Println("else-Body, x =", x)
    }
}
Output
if-Body, x = 10

Diese Form von absichtlichem Shadowing ist legitim, aber selten nötig. Wer das versehentlich tut, hat einen Bug.

comma-ok — der klassische Anwendungsfall

Drei Go-Sprachfeatures liefern „comma-ok"-Ergebnisse: Map-Lookups, Channel-Receives, Type Assertions. Alle drei landen idiomatisch in if-Init-Klauseln:

Go comma_ok.go
package main

import "fmt"

func main() {
    users := map[string]int{"alice": 30, "bob": 25}

    // Map-Lookup
    if age, ok := users["alice"]; ok {
        fmt.Println("alice ist", age)
    }

    // Negierte Variante — User fehlt
    if _, ok := users["carol"]; !ok {
        fmt.Println("carol unbekannt")
    }

    // Channel-Receive
    ch := make(chan int, 1)
    ch <- 42
    close(ch)
    if v, ok := <-ch; ok {
        fmt.Println("aus Channel:", v)
    }

    // Type Assertion
    var any interface{} = "hallo"
    if s, ok := any.(string); ok {
        fmt.Println("ist string:", s, "Länge", len(s))
    }
}
Output
alice ist 30
carol unbekannt
aus Channel: 42
ist string: hallo Länge 5

Drei Vorteile dieses Patterns:

  • Der prüfende Boolean (ok) lebt genau in der Verzweigung, in der er gebraucht wird.
  • Der gefundene Wert ist nur dann sichtbar, wenn er gültig ist — versehentliches Verwenden eines „nicht gesetzten" Werts ist ausgeschlossen.
  • Die Init-Form spart den := ... if ...-Zwei-Zeiler.

Guard Clauses — frühes Aussteigen

Das idiomatische Go-Pattern für Error-Handling ist die Guard Clause: prüfen, beim ersten Problem zurückkehren, sonst linear weiterarbeiten. Die Init-Form macht das knapp:

Go guard.go
package main

import (
    "errors"
    "fmt"
    "strconv"
)

func parseAge(s string) (int, error) {
    if s == "" {
        return 0, errors.New("leerer Input")
    }

    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("kein Integer: %w", err)
    }

    if n < 0 || n > 150 {
        return 0, fmt.Errorf("unplausibel: %d", n)
    }

    return n, nil
}

func main() {
    for _, s := range []string{"", "abc", "-1", "42"} {
        age, err := parseAge(s)
        if err != nil {
            fmt.Printf("input %q: %v\n", s, err)
            continue
        }
        fmt.Printf("input %q -> %d\n", s, age)
    }
}
Output
input "": leerer Input
input "abc": kein Integer: strconv.Atoi: parsing "abc": invalid syntax
input "-1": unplausibel: -1
input "42" -> 42

Beobachtungen:

  • Der „glückliche Pfad" liegt linear unten, ohne Einrückung. Lesbar wie Prosa.
  • Jede Fehler-Branche ist eine eigene if-Verzweigung, oft mit Init-Klausel — die Variablen aus der Prüfung sind in der Branche sichtbar, danach weg.
  • else taucht praktisch nie auf. Statt if err != nil { ... } else { weiterarbeiten } macht Go if err != nil { return ... }; weiterarbeiten — flacher und lesbarer.

else-if, else und die richtige Anzahl Zweige

Mehrere else if plus ein abschließendes else sind erlaubt — aber sobald die Kette länger als zwei oder drei wird, ist switch meist die bessere Wahl. Faustregel: maximal zwei else if. Drei oder mehr sind ein Signal für Refactoring.

Go else_if.go
package main

import "fmt"

func classify(score int) string {
    // OK — drei Zweige
    if score >= 90 {
        return "A"
    } else if score >= 75 {
        return "B"
    } else if score >= 60 {
        return "C"
    }
    return "F"
}

func classifySwitch(score int) string {
    // Bei mehr Bereichen: switch ist sauberer
    switch {
    case score >= 90:
        return "A"
    case score >= 75:
        return "B"
    case score >= 60:
        return "C"
    default:
        return "F"
    }
}

func main() {
    fmt.Println(classify(85), classifySwitch(85))
}
Output
B B

Ein subtiler Punkt: Bei einer langen if-else if-Kette mit Init-Klausel wird das Init-Statement nur einmal ausgewertet — vor dem ersten Test. Es läuft nicht erneut für jeden else if. Wer das nicht im Kopf hat, baut versehentlich Seiteneffekt-Code, der nur in der ersten Branche „landet":

Go init_once.go
package main

import "fmt"

var calls int

func nextValue() int {
    calls++
    return calls
}

func main() {
    if v := nextValue(); v == 1 {
        fmt.Println("erste Branche:", v)
    } else if v == 2 {
        fmt.Println("zweite Branche:", v)
    } else {
        fmt.Println("else:", v)
    }
    fmt.Println("Aufrufe insgesamt:", calls)
}
Output
erste Branche: 1
Aufrufe insgesamt: 1

nextValue() läuft genau einmal, nicht pro Test. Das ist Absicht und in der Spec klar geregelt — aber man muss es wissen.

Composite Literals — die Ambiguität in der Condition

Die Spec erwähnt eine sehr eigene Falle. Composite Literals (T{...}) in der Condition kollidieren mit Gos Block-Syntax, wenn der Compiler nicht weiß, ob die geschweifte Klammer den Body öffnet oder zum Literal gehört:

Go composite_ambiguity.go
package main

type Point struct{ X, Y int }

func main() {
    origin := Point{0, 0}

    // FEHLER: Compiler liest Point{ als Block-Anfang
    // if origin == Point{0, 0} { ... }

    // KORREKT: Klammern um das Composite Literal
    if origin == (Point{0, 0}) {
        println("origin")
    }

    // ALTERNATIVE: vorher einer Variable zuweisen
    zero := Point{0, 0}
    if origin == zero {
        println("origin via var")
    }
}

Dieselbe Ambiguität gilt für for und switch. Lösung: entweder Klammern um das Composite Literal, oder eine vorgeschobene Variable.

Init-Form vs. vorgeschobene Variable — wann was

Manchmal hat die Init-Form keinen Vorteil, sondern verschleiert. Eine Heuristik:

SituationForm
Variable nur in der if-Kette gebrauchtInit-Form
Variable danach noch nötigvar davor, dann if cond { ... }
comma-ok-Pattern (v, ok := ...)Init-Form
Funktion mit Error-Return, der Wert wird gebraucht:= ... davor, if err != nil { return ... } darunter — nicht Init-Form, damit der Wert weiterlebt
Komplexer Ausdruck mit mehreren SchrittenStatements davor, if einfach halten

Konkretes Gegenbeispiel — hier ist die Init-Form schlechter:

Go anti_pattern.go
// ANTI-PATTERN: Init-Form klaut uns den Wert
if data, err := readConfig(); err != nil {
    log.Fatal(err)
}
// data ist hier nicht mehr sichtbar — der Wert ist verloren.

// BESSER: Decl davor
data, err := readConfig()
if err != nil {
    log.Fatal(err)
}
useData(data)  // jetzt sichtbar

Faustregel: Wenn der Wert nach der Verzweigung noch gebraucht wird, gehört die Deklaration aus dem if heraus.

Klammern um die Condition — niemals

Anders als in C oder Java sind Klammern um die Condition in Go nicht idiomatisch. Sie sind nicht verboten — gofmt formatiert sie nicht weg — aber der Style-Guide und alle Codebases verzichten konsequent darauf. Wer von C/Java kommt, sollte die Gewohnheit ablegen:

Go parens.go
// Idiomatisches Go
if x > 5 && y < 10 {
    ...
}

// Legal, aber unidiomatisch — sieht nach C aus
if (x > 5 && y < 10) {
    ...
}

Klammern sind nur dort sinnvoll, wo die Präzedenz nicht offensichtlich ist (if (a || b) && c { ... }) oder zur Auflösung der oben gezeigten Composite-Literal-Ambiguität.

Häufige Stolperfallen

Shadowing in if-Init mit := — die häufigste Falle.

Wer x, err := foo() in einer if-Init schreibt, obwohl err schon im äußeren Scope existiert, legt eine neue err-Variable an, die mit der if-Kette stirbt. Die äußere bleibt unverändert. Bei reinem comma-ok-Check im if-Body unproblematisch — bei späterem Zugriff auf die äußere Variable bricht es subtil. Den shadow-Analyzer aktivieren.

Init-Form schluckt den Wert — danach unzugänglich.

if data, err := load(); err != nil { ... } macht data nach der Verzweigung unsichtbar. Wenn du den Wert weiterverwenden willst, gehört das := aus dem if heraus — sonst musst du ihn doppelt lesen oder neu berechnen.

Init-Statement läuft einmal für die ganze if-else-Kette.

if v := compute(); v == 1 { ... } else if v == 2 { ... } ruft compute() exakt einmal auf — nicht pro Branche. Wer Seiteneffekte erwartet, baut dadurch leise einen Bug. Wer das Verhalten will (Wert einmal berechnen, mehrfach prüfen), nutzt es bewusst.

Composite Literal in Condition braucht Klammern.

if p == Point{0, 0} { ... } ist Syntax-Fehler — der Compiler liest Point{ als Block-Anfang. Lösung: if p == (Point{0, 0}) { ... } oder vorher zero := Point{0, 0}; if p == zero { ... }. Gleiche Falle bei for und switch.

Geschweifte Klammern sind Pflicht — auch bei Einzeilern.

if x > 0 doSomething() ist Syntax-Fehler. if x > 0 { doSomething() } ist das Minimum. Das hat Go bewusst von C übernommen, um die berüchtigte „goto fail"-Klasse von Bugs zu vermeiden (versteckter Statement-Body hinter einem if ohne Block).

else-Body ist ein eigener Block — Variablen lassen sich shadowen.

Im else-Zweig kannst du x := -1 schreiben, auch wenn die Init-Klausel des if schon ein x deklariert hat. Das innere überdeckt das äußere. Ist legal, aber fast immer ein Versehen — gleicher Name im Sub-Block sollte Alarm auslösen.

else if ist syntaktischer Trick, kein eigenes Keyword.

Die Spec definiert nur if und elseelse if ist else <neue IfStmt>. Praktische Konsequenz: Es gibt keinen separaten Scope für „else if" — die Init-Variable aus dem ersten if ist in allen nachfolgenden else if/else-Zweigen sichtbar.

Lange else if-Ketten gehören in switch.

Drei oder mehr else if sind ein Refactoring-Signal. switch { case a: ... ; case b: ... } (ohne Tag) liest sich besser, gruppiert die Fälle klar und erlaubt fallthrough, wenn nötig. Auch der Type Switch (switch v := x.(type) { ... }) ersetzt manuelle if _, ok := x.(T); ok-Kaskaden.

Klammern um die Condition sind unidiomatisch.

if (x > 0) { ... } läuft, aber niemand schreibt das. gofmt formatiert nicht weg, aber jeder Code-Reviewer markiert es. Die Gewohnheit aus C/Java ablegen — Go braucht keine Parens, und Inkonsistenz im Codebase schmerzt.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Kontrollstrukturen

Zur Übersicht