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:
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.
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")
}
}groß
doppelt groß: 14
ungerade, Rest: 1Drei Eigenheiten, die Go von C/Java/JavaScript unterscheiden:
- Keine Klammern um die Condition.
if (n > 5) { ... }ist erlaubt, aber unidiomatisch —gofmtlä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.
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, andswitchstatement 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.
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
}groß, x = 10Das 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:
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)
}
}if-Body, x = 10Diese 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:
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))
}
}alice ist 30
carol unbekannt
aus Channel: 42
ist string: hallo Länge 5Drei 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:
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)
}
}input "": leerer Input
input "abc": kein Integer: strconv.Atoi: parsing "abc": invalid syntax
input "-1": unplausibel: -1
input "42" -> 42Beobachtungen:
- 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. elsetaucht praktisch nie auf. Stattif err != nil { ... } else { weiterarbeiten }macht Goif 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.
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))
}B BEin 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":
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)
}erste Branche: 1
Aufrufe insgesamt: 1nextValue() 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:
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:
| Situation | Form |
|---|---|
Variable nur in der if-Kette gebraucht | Init-Form |
| Variable danach noch nötig | var 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 Schritten | Statements davor, if einfach halten |
Konkretes Gegenbeispiel — hier ist die Init-Form schlechter:
// 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 sichtbarFaustregel: 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:
// 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 else — else 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
- If statements – Go Language Specification
- Declarations and scope – Go Specification
- Effective Go: If
- Go Code Review Comments: Indent Error Flow
go vetshadow analyzer