Gos switch ist nicht das langweilige switch aus C. Es hat keine implicit fallthrough-Falle, akzeptiert mehrere Werte pro Case, lebt mit einer optionalen Init-Klausel und kennt eine eigene Sonderform — den Type Switch — die aus einem Interface-Wert seinen dynamischen Typ herausfischt und gleichzeitig in einer typisierten Variable bereitstellt. Dazu gibt es das Tag-lose switch, das eine lange if-else if-Kette in eine flache, gut lesbare Tabelle verwandelt. Dieser Artikel arbeitet alle Formen formal durch — Grammar, Scoping, fallthrough, break, Type-Switch-Mechanik — und sammelt am Ende die Stolperfallen, die in echten Codebases regelmäßig auftauchen.
Zwei switch-Familien: Expression-Switch und Type-Switch
Die Go-Spec teilt switch in zwei syntaktisch ähnliche, semantisch verschiedene Formen auf — den Expression-Switch (Vergleich auf Werte) und den Type-Switch (Vergleich auf dynamische Typen eines Interface-Werts):
SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .
ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .
TypeSwitchStmt = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause = TypeSwitchCase ":" StatementList .
TypeSwitchCase = "case" TypeList | "default" .
TypeList = Type { "," Type } .Aus dieser Grammar ergeben sich vier praktisch relevante Varianten:
| Form | Kopf | Was wird verglichen? |
|---|---|---|
| Expression-Switch mit Tag | switch x { ... } | x gegen Case-Werte |
| Tag-loses Expression-Switch | switch { ... } | Boolesche Conditions pro Case |
| Expression-Switch mit Init | switch v := f(); v { ... } | v gegen Case-Werte, Init wie bei if |
| Type-Switch | switch v := x.(type) { ... } | Dynamischer Typ von x gegen Case-Typen |
Drei Eigenschaften gelten für alle Formen — sie heben Go von C/Java/JavaScript ab:
- Kein implizites fallthrough. Jede Case-Branche endet automatisch. Wer durchfallen will, schreibt
fallthroughexplizit. - Mehrere Werte pro Case erlaubt.
case 1, 2, 3:ersetzt drei einzelne Cases. - Jede Case-Branche ist ein eigener Block. Variablen aus
case 1:sind incase 2:nicht sichtbar.
Klassische Form mit Tag — switch x { case ... }
Die häufigste Form: ein Ausdruck im Kopf, eine Liste von Case-Werten darunter. Der Tag wird genau einmal ausgewertet, dann mit den Case-Werten von oben nach unten verglichen — der erste Treffer gewinnt:
package main
import "fmt"
func dayKind(day string) string {
switch day {
case "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag":
return "Werktag"
case "Samstag", "Sonntag":
return "Wochenende"
default:
return "unbekannt"
}
}
func main() {
for _, d := range []string{"Montag", "Samstag", "Funday"} {
fmt.Printf("%-8s -> %s\n", d, dayKind(d))
}
}Montag -> Werktag
Samstag -> Wochenende
Funday -> unbekanntBeobachtungen:
- Mehrere Werte pro Case werden durch Komma getrennt. Das ist ein logisches OR, kein AND —
case "Montag", "Dienstag":matcht, wenn der Tag einer dieser Werte ist. defaultist optional. Steht idiomatisch zuletzt, darf aber syntaktisch überall stehen. Genau einer proswitch.- Kein
breaknötig. Die Branche endet mit dem nächstencaseoder der schließenden Klammer.
Die Tag-Form ist nicht auf konstante Werte beschränkt — Case-Ausdrücke können beliebige Expressions sein, müssen aber vergleichbar mit dem Tag-Typ sein (mit ==). Konstanten-Cases werden zur Compile-Zeit auf Duplikate geprüft:
// FEHLER: duplicate case 1 in switch
// switch n {
// case 1:
// // ...
// case 1:
// // ...
// }
// OK — Cases sind Ausdrücke, der Compiler kann Dynamik nicht prüfen
switch n {
case f():
// ...
case f():
// bei gleichem Rückgabewert gewinnt der erste Case
}Tag-loses switch — der saubere if-else-Ersatz
Lässt du den Tag weg, wird switch zu einer Tabelle aus Bedingungen. Jeder Case ist dann ein boolescher Ausdruck — der erste, der true ergibt, wird ausgeführt. Implicit ist der Tag dabei true, das heißt switch { case cond: ... } ist semantisch identisch zu switch true { case cond: ... }.
package main
import "fmt"
func grade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 75:
return "B"
case score >= 60:
return "C"
case score >= 0:
return "F"
default:
return "ungültig"
}
}
func main() {
for _, s := range []int{95, 80, 65, 40, -1} {
fmt.Printf("%3d -> %s\n", s, grade(s))
}
} 95 -> A
80 -> B
65 -> C
40 -> F
-1 -> ungültigDas ist die idiomatische Alternative zu einer langen if-else if-Kette. Vorteile:
- Die Bedingungen stehen in einer optisch flachen Tabelle — kein Treppen-Layout.
defaultmarkiert den Fallback eindeutig — beiif/else ifmusste das ein freierelse-Zweig leisten.- Die Reihenfolge der Cases bleibt entscheidend (top-to-bottom, erster Treffer gewinnt), aber das Layout zeigt das klar.
Faustregel: Ab drei else if-Zweigen wechselt man auf Tag-loses switch.
Init-Klausel — switch v := f(); v { ... }
Wie if und for erlaubt switch eine optionale Init-Klausel vor dem Tag, abgetrennt durch ein Semikolon. Die dort deklarierte Variable lebt im gesamten switch-Block — also in allen Cases und im optionalen default. Außerhalb ist sie weg.
package main
import (
"fmt"
"time"
)
func greetingFor(t time.Time) string {
switch h := t.Hour(); {
case h < 6:
return "noch Nacht"
case h < 12:
return "Guten Morgen"
case h < 18:
return "Guten Tag"
default:
return "Guten Abend"
}
}
func main() {
for _, h := range []int{2, 9, 14, 20} {
t := time.Date(2026, 5, 16, h, 0, 0, 0, time.UTC)
fmt.Printf("%02d Uhr -> %s\n", h, greetingFor(t))
}
}02 Uhr -> noch Nacht
09 Uhr -> Guten Morgen
14 Uhr -> Guten Tag
20 Uhr -> Guten AbendDrei Punkte zur Init-Klausel:
- Sie ist syntaktisch ein SimpleStatement — also
:=,=, Funktionsaufruf,x++/x--, Send. Keine Blockstatements. - Sie läuft einmal, vor jedem Case-Vergleich. Seiteneffekte landen nur einmal, nicht pro Case.
- Sie kombiniert sich mit jeder Tag-Form:
switch v := f(); v { ... }(mit Tag),switch v := f(); { ... }(tag-los),switch v := x.(type) { ... }(Type-Switch — Init ist hier der Type-Switch-Guard selbst).
fallthrough — explizit, nie versehentlich
Gos default-Verhalten ist das Gegenteil von C: Jede Case-Branche endet automatisch. Wer durchfallen will, schreibt fallthrough als letztes Statement der Branche — der Compiler erzwingt diese Position.
package main
import "fmt"
func describe(n int) {
switch n {
case 1:
fmt.Println("eins")
fallthrough
case 2:
fmt.Println("eins oder zwei")
fallthrough
case 3:
fmt.Println("eins, zwei oder drei")
case 4:
fmt.Println("vier (eigene Branche)")
default:
fmt.Println("etwas anderes")
}
}
func main() {
describe(1)
fmt.Println("---")
describe(2)
fmt.Println("---")
describe(4)
}eins
eins oder zwei
eins, zwei oder drei
---
eins oder zwei
eins, zwei oder drei
---
vier (eigene Branche)Drei Eigenheiten von fallthrough:
- Übergibt unbedingt an die nächste Case-Branche. Der Case-Ausdruck der nächsten Branche wird nicht neu geprüft — das Statement-List dort läuft so oder so.
- Muss letztes Statement der Branche sein. Davor
return,breakoder Code dahinter ist Compile-Fehler. - Im Type-Switch verboten.
fallthroughwäre dort sinnlos, weil der typisierte Wert in der nächsten Branche einen anderen Typ hätte.
In echtem Code ist fallthrough selten — meistens lässt sich das gleiche Ergebnis sauberer durch mehrere Werte in einem Case (case 1, 2, 3:) oder eine kleine Hilfsfunktion erreichen.
break — selten, aber manchmal nötig
Weil Cases nicht implizit durchfallen, brauchst du break in switch nicht zum Beenden einer Branche. Es hat trotzdem einen sinnvollen Einsatz: den vorzeitigen Abbruch einer Branche aus einem inneren if heraus.
package main
import "fmt"
func process(items []int, target int) {
switch len(items) {
case 0:
fmt.Println("leer")
default:
for _, v := range items {
if v == target {
fmt.Println("gefunden:", v)
break // bricht NUR die for-Schleife ab, nicht den switch
}
}
fmt.Println("Suche fertig")
}
}
func main() {
process([]int{}, 42)
process([]int{1, 2, 42, 99}, 42)
}leer
gefunden: 42
Suche fertigWichtig: break bezieht sich auf die innerste umschließende for-, switch- oder select-Anweisung. Das obige Beispiel zeigt die Falle: break innerhalb der for bricht die Schleife ab, nicht den switch. Wer den switch von innen heraus verlassen will, braucht ein Label:
package main
import "fmt"
func main() {
items := []int{1, 2, 42, 99}
Outer:
switch len(items) {
case 0:
fmt.Println("leer")
default:
for _, v := range items {
if v == 42 {
fmt.Println("Treffer — switch verlassen")
break Outer // bricht den switch ab, nicht die for
}
}
fmt.Println("würde laufen, wenn break Outer nicht griff")
}
fmt.Println("nach dem switch")
}Treffer — switch verlassen
nach dem switchbreak Outer springt aus dem switch heraus. Ohne Label wäre nur die for betroffen, und die abschließende fmt.Println("würde laufen, ...") hätte trotzdem gefeuert.
Type Switch — switch v := x.(type) { ... }
Der Type-Switch ist Gos idiomatischer Weg, aus einem Interface-Wert seinen dynamischen Typ herauszufischen — und gleichzeitig eine korrekt typisierte Variable zu bekommen. Er ist die strukturierte Variante einer Kaskade aus if _, ok := x.(T); ok { ... }-Type-Assertions.
package main
import "fmt"
func describe(x any) {
switch v := x.(type) {
case nil:
fmt.Println("ist nil")
case bool:
fmt.Printf("bool: %v\n", v)
case int:
fmt.Printf("int: %d (verdoppelt %d)\n", v, v*2)
case string:
fmt.Printf("string: %q (Länge %d)\n", v, len(v))
case []byte:
fmt.Printf("byte-Slice der Länge %d\n", len(v))
default:
fmt.Printf("unbekannter Typ: %T\n", v)
}
}
func main() {
describe(nil)
describe(true)
describe(42)
describe("hallo")
describe([]byte{1, 2, 3})
describe(3.14)
}ist nil
bool: true
int: 42 (verdoppelt 84)
string: "hallo" (Länge 5)
byte-Slice der Länge 3
unbekannter Typ: float64Wichtig dabei:
- Der Guard
x.(type)ist ausschließlich im Kopf einesswitcherlaubt — er ist keine normale Type-Assertion. xmuss Interface-Typ sein. Über konkrete Typen kann man keinen Type-Switch fahren — das wäre Tautologie, der Typ steht ja schon zur Compile-Zeit fest.- Die deklarierte Variable (hier
v) hat in jeder Case-Branche den dort gelisteten Typ. In dercase int:-Branche istveinint, incase string:einstring, indefault:der ursprüngliche Interface-Typ. case nil:fängt den nil-Interface-Fall ab. Praktisch wichtig — sonst landetnilindefault.
Der Type-Switch funktioniert auch ohne benannte Variable: switch x.(type) { case int: ... }. Dann hast du keinen typisierten Wert, sondern nur den Typ-Vergleich. Selten nützlich — meistens willst du den Wert auch.
Type Switch mit Mehrfach-Cases — der Interface-Typ-Sonderfall
Wenn ein case mehrere Typen listet, hat die deklarierte Variable nicht den jeweils gematchten Typ, sondern den Interface-Typ des Type-Switch-Guards. Das ist nötig, weil die Variable sonst in der Branche zwei verschiedene Typen gleichzeitig haben müsste.
package main
import "fmt"
func numericInfo(x any) {
switch v := x.(type) {
case int, int32, int64:
// v hat hier den Typ `any` — NICHT int/int32/int64!
// arithmetische Operationen direkt auf v sind nicht möglich.
fmt.Printf("ganzzahlig (Interface-Typ): %v\n", v)
case float32, float64:
fmt.Printf("gleitkomma (Interface-Typ): %v\n", v)
case int8:
// Einziger Typ — v ist hier int8
fmt.Printf("int8 (konkret): %d, verdoppelt: %d\n", v, v*2)
default:
fmt.Printf("kein numerischer Typ: %T\n", v)
}
}
func main() {
numericInfo(42)
numericInfo(int32(7))
numericInfo(int8(3))
numericInfo(3.14)
numericInfo("nope")
}ganzzahlig (Interface-Typ): 42
ganzzahlig (Interface-Typ): 7
int8 (konkret): 3, verdoppelt: 6
gleitkomma (Interface-Typ): 3.14
kein numerischer Typ: stringKonsequenz fürs Design: Wenn du den typisierten Wert brauchst (z. B. um arithmetisch weiterzurechnen), verteile die Typen auf separate Cases. Wenn dir der Typ-Bucket genügt (z. B. um eine Logmeldung zu drucken), darfst du gruppieren.
Wer den konkreten Typ trotzdem braucht, kann innerhalb der Branche eine nested Type-Assertion machen:
switch v := x.(type) {
case int, int32, int64:
// v ist any — re-asserten
switch n := v.(type) {
case int:
fmt.Println("int:", n*2)
case int32:
fmt.Println("int32:", n*2)
case int64:
fmt.Println("int64:", n*2)
}
}Doppelter Aufwand — meistens ist das ein Signal, die Cases gleich auseinanderzuziehen.
Praxis-Patterns — wann switch glänzt
Drei wiederkehrende Einsatzgebiete, in denen switch (Expression oder Type) klar gewinnt:
State-Machine. Endliche Zustände, klare Übergänge — Tag-Form mit default als „unmöglicher Zustand":
package main
import "fmt"
type State int
const (
StateIdle State = iota
StateRunning
StatePaused
StateDone
)
func (s State) next(event string) State {
switch s {
case StateIdle:
if event == "start" {
return StateRunning
}
case StateRunning:
switch event {
case "pause":
return StatePaused
case "finish":
return StateDone
}
case StatePaused:
if event == "resume" {
return StateRunning
}
case StateDone:
// terminaler Zustand — keine Übergänge
}
return s
}
func main() {
s := StateIdle
for _, e := range []string{"start", "pause", "resume", "finish"} {
s = s.next(e)
fmt.Printf("%-7s -> %d\n", e, s)
}
}start -> 1
pause -> 2
resume -> 1
finish -> 3Klassifizierung über Bereiche. Tag-loses switch mit Range-Conditions — sauberer als drei else if:
func classifySize(n int) string {
switch {
case n < 0:
return "negativ"
case n == 0:
return "null"
case n < 100:
return "klein"
case n < 10_000:
return "mittel"
default:
return "groß"
}
}Polymorphismus über Type-Switch. Wenn ein Interface mehrere konkrete Implementierungen hat und der Verarbeiter typabhängig reagieren muss — etwa beim Walk durch einen AST oder einen JSON-Wert:
package main
import (
"encoding/json"
"fmt"
)
func walk(prefix string, v any) {
switch x := v.(type) {
case map[string]any:
for k, child := range x {
walk(prefix+"."+k, child)
}
case []any:
for i, child := range x {
walk(fmt.Sprintf("%s[%d]", prefix, i), child)
}
case string:
fmt.Printf("%s = %q\n", prefix, x)
case float64:
fmt.Printf("%s = %g\n", prefix, x)
case bool:
fmt.Printf("%s = %v\n", prefix, x)
case nil:
fmt.Printf("%s = null\n", prefix)
default:
fmt.Printf("%s = ? (%T)\n", prefix, x)
}
}
func main() {
raw := `{"name":"alice","age":30,"tags":["a","b"]}`
var v any
_ = json.Unmarshal([]byte(raw), &v)
walk("root", v)
}root.name = "alice"
root.age = 30
root.tags[0] = "a"
root.tags[1] = "b"Das ist klassischer Type-Switch-Code: encoding/json liefert beim Unmarshal in any exakt die sechs JSON-Typen — und der Type-Switch dekomponiert sie wieder.
Was Go bewusst NICHT macht — kein Pattern Matching
Wer aus Rust, Swift, Scala oder OCaml kommt, hat den Reflex, switch als Pattern-Match zu lesen. Das ist in Go nicht so:
- Cases sind Werte oder Typen, keine Patterns. Du kannst nicht
case Point{X: 0, Y: _}schreiben — das wäre ein Struct-Pattern, das Go nicht kennt. - Keine Guards an Cases.
case n where n > 0:gibt es nicht. Wer das will, nimmt Tag-losesswitchund packt die Bedingung in den Case-Ausdruck. - Keine Bindings im Pattern. Im Type-Switch bindet
v := x.(type)einmal — pro Branche existiert die Variable mit dem passenden Typ, aber sie zerlegt die Struktur nicht weiter.
Pragmatisch heißt das: Wer Pattern-Match-Stil sucht, kombiniert switch, Type-Assertion und kleine Hilfsfunktionen. Das wird selten so elegant wie in Rust, ist aber lesbar und vollständig.
Häufige Stolperfallen
fallthrough wird oft vergessen — oder unnötig genutzt.
Gos Default ist „Branche endet automatisch". Wer aus C kommt, vergisst das anfangs nicht, sondern erwartet das Gegenteil — und wundert sich, warum case 1: case 2: nicht „matcht für 1 und 2". Lösung: case 1, 2: (Komma-OR). fallthrough ist nur für die seltene Sequenz „nach Case 1 auch Code von Case 2 ausführen, ungeprüft" gedacht — meistens umschreibt eine Hilfsfunktion das sauberer.
default kann überall stehen — idiomatisch ist es zuletzt.
Syntaktisch ist default einfach noch ein Case. Er darf ganz oben, in der Mitte oder unten stehen — die Ausführung ist davon unabhängig, weil default erst greift, wenn kein anderer Case matcht. Lesbar ist nur die Position zuletzt — alles andere irritiert Code-Reviewer.
Kein Pattern Matching wie in Rust oder Swift — Cases sind Werte/Typen.
case Point{0, 0}: matcht keinen Struct-Pattern — der Compiler liest das als Composite-Literal-Wert und vergleicht mit ==. Destructuring (case Point{X: x, Y: 0}:) gibt es gar nicht. Wer Strukturen zerlegen will, packt das in eine Hilfsfunktion oder in if-Conditions.
Type-Switch mit Mehrfach-Typ-Case: v hat dann Interface-Typ.
case int, int64: macht v zum Interface-Typ des Guards (meist any), nicht zum gematchten konkreten Typ — arithmetische Operationen schlagen fehl. Wer den konkreten Typ braucht, listet die Typen in eigenen Cases.
case a, b, c: ist OR, nicht AND.
Mehrere Werte in einem Case sind eine Oder-Liste. case 1, 2: matcht, wenn der Tag 1 oder 2 ist. Wer „matcht, wenn beide Bedingungen gelten" sucht, kombiniert das in einem booleschen Ausdruck in einem Tag-losen switch.
Composite-Literal-Ambiguität im switch-Tag.
switch x { case Point{0, 0}: ... } ist okay — aber switch Point{0, 0} { case ...: ... } wird vom Parser falsch zerlegt, weil die geschweifte Klammer als Body-Beginn gelesen wird. Lösung: Klammern (switch (Point{0, 0}) { ... }) oder vorher einer Variable zuweisen.
Type-Switch nur über Interfaces — nicht über konkrete Typen.
switch v := s.(type) mit s als string ist Compile-Fehler — der Typ steht zur Compile-Zeit fest, es gibt nichts zu unterscheiden. Type-Switch macht nur Sinn, wenn der statische Typ ein Interface ist (oft any).
Case-Reihenfolge bei Type-Switch — Interfaces nach konkreten Typen.
Wenn ein Type-Switch ein Interface (z. B. error) und einen konkreten Typ, der das Interface implementiert (z. B. *myError), gleichzeitig prüft, muss der konkretere Typ zuerst kommen. Cases werden top-to-bottom geprüft — sonst schluckt der Interface-Case alle Implementierungen, bevor der konkrete drankommt.
Duplikate Case-Werte sind Compile-Fehler bei konstanten Cases.
case 1: ... case 1: ... bricht den Build mit duplicate case 1 in switch. Bei dynamischen Case-Ausdrücken (Funktionsaufrufen) kann der Compiler das nicht prüfen — dort gewinnt einfach der erste matchende, der zweite ist toter Code.
Weiterführende Ressourcen
Externe Quellen
- Switch statements – Go Language Specification
- Expression switches – Go Specification
- Type switches – Go Specification
- Effective Go: Switch
- Effective Go: Type switch