navigation Navigation


Inhaltsverzeichnis

Kontrollstrukturen


Kontrollstrukturen geben Go die nötige Klarheit und Vorhersehbarkeit. if-Verzweigungen decken einfache Entscheidungen ab, switch fasst Zustände elegant zusammen, und for bildet das flexible Rückgrat für Iterationen. select steuert nebenläufige Abläufe präzise, indem es Kommunikationswege zwischen Goroutines koordiniert. Zusammen ermöglichen diese Bausteine robuste, gut lesbare und wartbare Anwendungen.

Inhaltsverzeichnis

    Einführung

    Kontrollstrukturen sind die Bausteine, die den Ablauf eines Programms steuern. Sie bestimmen, welche Code-Abschnitte ausgeführt werden, wie oft sie wiederholt werden und unter welchen Bedingungen bestimmte Aktionen stattfinden. In Go gibt es eine überschaubare, aber mächtige Sammlung von Kontrollstrukturen, die sich durch ihre Einfachheit und Klarheit auszeichnet.

    Im Gegensatz zu vielen anderen Programmiersprachen hat Go bewusst auf einige Kontrollstrukturen verzichtet, um die Sprache schlank und vorhersehbar zu halten.

    Das bedeutet unter Anderem:

    • Keine while Schleife: Go hat nur for, das aber flexibel genug ist, um alle Schleifen-Anforderungen abzudecken.
    • Keine do-while Schleife: Auch dies wird durch for abgedeckt.
    • Keine ternären Operatoren (? :): Stattdessen wird if-else verwendet.
    • Kein automatisches Fallthrough in switch: Im Gegensatz zu C/C++/Java ist break nicht nötig.

    Diese bewussten Einschränkungen machen Go-Code lesbarer und weniger fehleranfällig.

    if / else - Bedinge Ausführung

    Grundlegende Syntax

    Die if Anweisung führt einen Code-Block nur aus, wenn eine Bedingung true (also erfüllt) ist.

    Syntax

    if bedingung {
        // Code wird ausgeführt, wenn true
    }

    Wichtige Punkte

    • Keine Klammern um die Bedingung: In Go sind () um die Bedingung optional und werden idiomatisch weggelassen.
    • Geschweifte Klammern sind Pflicht: {} müssen immer verwendet werden, auch bei einzeiligem Code
    • Öffnende Klammer auf der gleichen Zeile: { muss auf derselben Zeile wie if stehen (Go-Syntax-Regel)
    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        age := 18
    
        if age >= 18 {
            fmt.Println("Du bist volljährig")
        }
    }

    if-else Verzweigung

    Mit else kann ein alternativer Code-Block angegeben werden, der ausgeführt wird, wenn die Bedingung false ist.

    Syntax

    if bedingung {
        // Code wenn true
    } else {
        // Code wenn false
    }

    Beispiel

    package main
    
    import "fmt"
    
    func main() {
        num := 100
    
        if num >= 200 {
            fmt.Println("Nummer ist größer 200")
        } else {
            fmt.Println("Nummer ist kleiner 200")
        }
    }

    else if - Mehrfachverzweigung

    Mit else if können mehrere Bedingungen nacheinander geprüft werden.

    Syntax

    if bedingung1 {
        // Code wenn bedingung1 true
    } else if bedingung2 {
        // Code wenn bedingung2 false
    } else {
        // Code wenn keine Bedingung true
    }

    Beispiel

    package main
    
    import "fmt"
    
    func main() {
        score := 85
    
        if score >= 90 {
            fmt.Println("Note: Sehr gut")
        } else if score >= 80 {
            fmt.Println("Note: Gut")
        } else if score >= 70 {
            fmt.Println("Note: Befriedigend")
        } else if score >= 60 {
            fmt.Println("Note: Ausreichend")
        } else {
            fmt.Println("Note: Nicht bestanden")
        }
    }

    if mit Initialisierungs-Statement

    Go erlaubt eine besondere Syntax: Man kann eine Variable direkt in der if Anweisung initialisieren. Diese Variable ist dann nur im Scope des if Blocks (und else Blocks) verfügbar.

    Syntax

    if initialisierung; bedingung {
        // Code
    }

    Beispiel

    package main
    
    import "fmt"
    
    func main() {
        if age := 20; age >= 18 {
            fmt.Println("Volljährig, Alter:", age)
        }
    }

    Ohne dieser Kurz-Initialisierung könnte man die gleiche Logik auch wie folgt schreiben.

    package main
    
    import "fmt"
    
    func main() {
        age := 20
        if age >= 18 {
            fmt.Println("Volljährig, Alter:", age)
        }
    }

    Wichtige Punkte:

    • Die Variable age existiert nur innerhalb des if Blocks (und des zugehörigen else)
    • Nach dem if ist age nicht mehr zugänglich

    Schauen wir uns ein praktisches Beispiel mit Fehlerbehandlung. Dieses Pattern wird in Go sehr oft angewendet.

    Beispiel
    package main
    
    import (
        "fmt"
        "strconv"
    )
    
    func main() {
        input := "123"
    
        if num, err := strconv.Atoi(input); err == nil {
            fmt.Println("Erfolgreich konvertiert:", num)
            fmt.Println("Doppelt:", num*2)
        } else {
            fmt.Println("Fehler:", err)
        }
    }
    Erfolgreich konvertiert: 123
    Doppelt: 246

    Was passiert in diesem Beispiel?

    1. strconv.Atoi(input) wird aufgerufen und gibt zwei Werte zurück: num und err
    2. Die Bedingung err == nil prüft, ob kein Fehler aufgetreten ist
    3. Wenn err == nil (kein Fehler), wird der if Block ausgeführt
    4. Die Variablen num und err sind nur im if und else Block verfügbar

    Verschachtelte if-Anweisungen

    if Anweisungen können beliebig verschachtelt werden.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        age := 20
        hasLicense := true
    
        if age >= 18 {
            if hasLicense {
                fmt.Println("Du darfst Auto fahren")
            } else {
                fmt.Println("Du musst noch einen Führerschein machen")
            }
        } else {
            fmt.Println("Du bist zu jung zum Autofahren")
        }
    }
    Du darfst Auto fahren.

    Diese Logik können wir auch mit logischen Operatoren schreiben. Verschachtelte if Anweisungen können oft durch logische Operatoren (&& , ||) vereinfacht werden.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        age := 20
        hasLicense := true
    
        if age >= 18 && hasLicense {
            fmt.Println("Du darfst Auto fahren")
        } else if age >= 18 {
            fmt.Println("Du musst noch den Führerschein machen")
        } else {
            fmt.Println("Du bist zu jung zum Autofahren")
        }
    }

    Early Returns (Guard Clauses)

    Ein wichtiges Pattern in Go ist das Early Return oder Guard Clause Pattern. Anstatt tief verschachtelte if-else Strukturen zu verwenden, prüft man zuerst auf Fehlerbedingungen und kehrt früh zurück.

    Im Folgenden schauen wir uns zwei Beispiele an. Das erste Beispiel ist funktionsfähig, aber weniger gut von der Struktur. Im zweiten Beispiel schauen wir uns, wie man es eleganter lösen kann.

    Schlechtes Beispiel (viele Verschachtelung)

    Beispiel - Schlecht
    func processUser(user *User) error {
        if user != nil {
            if user.IsActive {
                if user.HasPermission("write") {
                    // Eigentliche Logik
                    return doSomething(user)
                } else {
                    return errors.New("keine Berechtigung")
                }
            } else {
                return errors.New("Benutzer ist inaktiv")
            }
        } else {
            return errors.New("Benutzer ist nil")
        }
    }

    Im Vergleich dazu nun das Beispiel mit Early Returns.

    Beispiel - Gut
    func processUser(user *User) error {
        if user == nil {
            return errors.New("Benutzer ist nil")
        }
    
        if !user.IsActive {
            return errors.New("Benutzer ist inaktiv")
        }
    
        if !user.HasPermission("write") {
            return errors.New("keine Berechtigung")
        }
    
        // Eigentliche Logik
    
        return doSomething(user)
    }

    Vorteile:

    • Weniger Verschachtelung: Der Code bleibt flach und ist leichter zu lesen
    • Klare Fehlerbehandlung: Fehlerfälle werden zuerst abgearbeitet
    • Klare Reihenfolge: Die Hauptlogik steht am Ende ohne Verschachtelung

    Bedingungen und Wahrheitswerte

    Was ist eine gültige Bedingung?

    In Go muss eine Bedingung immer vom Typ bool sein. Im Gegensatz zu Sprachen wie C oder JavaScript gibt es keine implizite Konvertierung zu Boolean.

    Folgender Code würde in Go nicht funktionieren.

    x := 5
    if x {
        fmt.Println("x ist wahr")
    }

    Dagegen würde dieser Code korrekt funktionieren.

    x := 5
    if x != 0 {
        fmt.Println("x ist wahr")
    }

    Vergleichsoperatoren

    • == - gleich
    • != - ungleich
    • < - kleiner
    • <= - kleiner oder gleich
    • > - größer
    • >= - größer oder gleich

    Logische Operatoren

    • && - logisches UND (beide Seiten müssen true sein)
    • || - logisches ODER (mindestens eine Seite muss true sein)
    • ! - logisches NICHT (negiert den Wahrheitswert)

    Beispiel

    package main
    
    import "fmt"
    
    func main() {
        age := 25
        income := 30000
    
        // UND: Beide Bedinungen müssen true sein
        if age >= 18 && income >= 25000 {
            fmt.Println("Kreditantrag genehmigt")
        }
    
        // ODER: Mindestens eine Bedingung muss true sein
        if age < 18 || income < 25000 {
            fmt.Println("Weitere Prüfung erforderlich")
        }
    
        // NICHT: Negation
        isMinor := age < 18
        if !isMinor {
            fmt.Println("Volljährig")
        }
    }

    Short-Circuit Evaluation

    Go verwendet Short-Circuit Evaluation bei && und ||.

    • Bei &&: Wenn die linke Seite false ist, wird die rechte Seite nicht ausgewertet
    • Bei ||: Wenn die linke Seite true ist, wird die rechte Seite nicht ausgewertet
    Beispiel
    package main
    
    import "fmt"
    
    func isPositive(n int) bool {
        fmt.Println("isPositive aufgerufen mit", n)
        return n > 0
    }
    
    func main() {
        // Linke Seite ist false
        // Rechte Seite wird nicht ausgewertet
        if false && isPositive(5) {
            fmt.Println("Dieser Code wird nie ausgeführt")
        }
    
        // Linke Seite ist true
        // Rechte Seite wird nicht ausgewertet
        if true || isPositive(5) {
            fmt.Println("True")
        }
    }

    switch - Mehrfachverzweigung

    Die switch Anweisung ist eine elegante Alternative zu langen if-else Ketten. Go’s switch ist mächtiger und flexibler als in vielen anderen Sprachen.

    Expression Switch

    Ein Expression Switch vergleicht einen Ausdruck mit verschiedenen Werten.

    Syntax

    switch ausdruck {
    case wert1:
        // Code wenn ausdruck == wert1
    
    case wert2:
        // Code wenn ausdruck == wert2
    
    default:
        // Code wenn keine case-Bedingung
    }
    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        day := "Montag"
    
        switch day {
        case "Montag":
            fmt.Println("Start der Woche")
    
        case "Freitag":
            fmt.Println("Fast Wochenende")
    
        case "Samstag", "Sonntag":
            fmt.Println("Wochenende!")
        
        default:
            fmt.Println("Ein normaler Tag")
        }
    }

    Was passiert hier

    1. Der Wert von day wird ausgewertet (“Montag”)
    2. Go vergleicht day mit jedem case von oben nach unten
    3. Der erste zutreffende case wird ausgeführt
    4. Kein automatisches Fallthrough - nach dem case wird der switch automatisch beendet (kein break nötig)

    Auch, wenn kein break notwendig ist, kann es dennoch verwendet werden, um einen case Block vorzeitig zu verlassen.


    switch mit Initialisierungs-Statement

    Wie bei if kann auch switch eine Initialisierung haben.

    Syntax

    switch initialisierung; ausdruck {
    case wert1:
        // Code wenn ausdruck == wert1
    
    default:
        // Code wenn keine case-Bedingung
    }

    Zudem kann es auch ein switch ohne Verwendung von Ausdruck geben. Dabei wird der Wert aus der Initialisierung auf cases überprüft.

    Das ; Semikolon trennt die Initialisierung vom Ausdruck.

    Im nachfolgenden Beispiel werden wir nur die Variable aus der Initialisierung verwenden.

    Beispiel
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        switch hour := time.Now().Hour(); {
        case hour < 12:
            fmt.Println("Guten Morgen")
        
        case hour < 18:
            fmt.Println("Guten Tag")
    
        default:
            fmt.Println("Guten Abend")
        }
    }
    Guten Morgen

    Was genau passiert hier?

    Wir definieren eine Variable hour := time.Now().Hour(). Das Init-Statement wird ausgeführt. Nach dem Semikolon folgt kein expliziter Switch-Ausdruck. In Go bedeutet ein fehlender Switch-Ausdruck implizit true. Die case Statements werden als boolesche Ausdrücke ausgewertet.

    Das obere Konstrukt ist äquivalent zu folgendem Code:

    switch hour := time.Now().Hour(); true {
    
    }

    switch ohne Bedingung

    Eine besonders mächtige Variant ist switch ohne Ausdruck. Dies ist eine elegante Alternative zu if-else Ketten.

    Syntax

    switch {
    case bedingung1:
        // Code
    case bedingung2:
        // Code
    default:
        // Code
    }

    Jeder case enthält seine eigene Bedingung (anstatt einen Vergleichswert).

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        score := 85
    
        switch {
        case score >= 90:
            fmt.Println("Sehr gut")
        case score >= 80:
            fmt.Println("Gut")
        case score >= 70:
            fmt.Println("Befriedigend")
        case score >= 60:
            fmt.Println("Ausreichend")
        default:
            fmt.Println("Nicht bestanden")
        }
    }

    Warum kann es nützlich sein?

    • Übersichtlicher als lange if-else Ketten
    • Jeder case kann eine unterschiedliche Bedingung haben
    • Erster zutreffender case wird ausgeführt

    Fallthrough - Explizites Durchfallen

    Wenn man das Fallthrough-Verhalten von C/Java benötigt, kann man das Schlüsselwort fallthrough verwenden.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        num := 1 
    
        switch num {
        case 1:
            fmt.Println("Eins")
            fallthrough // Fällt in den nächsten Case
        case 2:
            fmt.Println("Zwei")
            fallthrough
        case 3:
            fmt.Println("Drei")
        default:
            fmt.Println("Standard")
        }
    }
    Eins
    Zwei
    Drei

    Was ist hier passiert?

    1. case 1 trifft zu => “Eins” wird ausgegeben
    2. fallthrough sorgt dafür, dass case 2 ebenfalls ausgeführt wird (ohne Bedingungsprüfung)
    3. Wieder fallthrough => case 3 wird ausgeführt
    4. Kein fallthrough mehr => switch endet

    Wichtig zu merken:

    • fallthrough führt den nächsten case bedingungslos aus (ohne die case Bedingung zu prüfen)
    • fallthrough kann nicht im letzten case oder default verwendet werden
    • fallthrough wird in der Praxis selten verwendet

    Type Switch - Interface-Typ-Prüfung

    Ein Type Switch prüft den dynamischen Typ eines Interface-Werts. Dies ist besonders dann nützlich, wenn man mit Interfaces arbeitet.

    Syntax

    switch v := x.(type) {
    case Typ1:
        // v ist vom Typ Typ1
    case Typ2:
        // v ist vom Typ Typ2
    default:
        // v ist vom ursprünglichen Typ
    }

    Schauen wir uns das Ganze an einem Beispiel an.

    Beispiel
    package main
    
    import "fmt"
    
    func describe(i any) {
        switch v := i.(type) {
        case int, int64:
            fmt.Printf("Ganzzahl: %d\n", v)
        case string:
            fmt.Printf("String: %s\n", v)
        case bool:
            fmt.Printf("Boolean: %t\n", v)
        default:
            fmt.Printf("Unbekannter Typ: %T\n", v)
        }
    }
    
    func main() {
        describe(42)
        describe("Hallo")
        describe(true)
        describe(3.14)
    }
    Ganzzahl: 42
    String: Hallo
    Boolean: true
    Unbekannter Typ: float64

    Wichtig:

    • i.(type) kann nur in switch Anweisungen verwendet werden
    • In jedem case hat v automatisch den richtigen Typ (keine manuelle Type-Assertion nötig)

    default in Switch

    Der default Fall wird ausgeführt, wenn keine der case Bedingungen zutrifft.

    Eigenschaften:

    • default ist optional
    • default kann an beliebiger Stelle im switch stehen (üblich jedoch am Ende)
    • Es kann nur einen default geben
    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        x := 10
    
        switch x {
        case 1:
            fmt.Println("Eins")
        case 5:
            fmt.Println("Fünf")
        default:
            fmt.Println("Etwas anders:", x)
        case 20:
            fmt.Println("Zwanzig")
        }
    }
    Etwas anderes: 10

    for - Die einzige Schleife

    Go hat nur eine Schleife: for. Diese ist jedoch so flexibel, dass sie alle gängigen Schleifen-Patterns abdeckt.

    • Klassische C-Style-Schleife: (for i := 0; i < 10; i++)
    • Bedingungsschleife (while Ersatz)
    • Endlosschleife
    • Range-Schleife (über Collections iterieren)

    Klassische Schleife (C-Style)

    Syntax

    for initialisierung; bedingung; post {
        // Code
    }

    Gehen wir kurz auf beteiligten Komponenten ein.

    • Initialisierung: Wird einmal am Anfang ausgeführt (z.B. i := 0)
    • Bedingung: Wird vor jeder Iteration geprüft (Schleife läuft, solange true)
    • Post: Wird nach jeder Iteration ausgeführt (z.B. i++)
    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        for i := 0; i < 5; i++ {
            fmt.Println("Iteration:", i)
        }
    }
    Iteration: 0
    Iteration: 1
    Iteration: 2
    Iteration: 3
    Iteration: 4

    Die Variable i (in diesem Beispiel) existiert nur innerhalb der Schleife.

    Scope von Schleifenvariablen

    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }
    
    // i ist hier nicht mehr verfügbar!

    Mehrere Variablen

    Es besteht auch die Möglichkeit, for-Schleifen mit mehreren Variablen zu verwenden.

    for i, j := 0, 10; i < j; i, j = i+1, j-1 {
        fmt.Printf("i=%d, j=%d\n", i, j)
    }

    An dieser Stelle lohnt sich ein genauer Blick auf das, was hier passiert.

    Initialisierung (einmalig am Anfang)

    i, j := 0, 10

    Hier werden zwei Variablen deklariert und initialisiert.

    • i erhält den Wert 0
    • j erhält den Wert 10

    Dieser Vorgang geschieht nur einmal vor dem ersten Schleifendurchlauf.

    Erste Prüfung der Bedingung

    i < j // 0 < 10 => true

    Die Schleife wird ausgeführt, da die Bedingung wahr ist.

    Erster Schleifendurchlauf

    fmt.Printf("i=%d, j=%d\n", i, j)
    
    // Ausgabe: i=0, j=10

    Nachiteration

    i, j = i+1, j-1
    
    // i = 0+1 = 1
    // j = 10-1 = 9

    Die Besonderheit bei dieser Art der Schleife ist, dass die Werte gleichzeitig aktualisiert werden und die Schleife terminiert, wenn i und j sich in der Mitte treffen.


    Condition-only Schleife (while Ersatz)

    Go hat keine while Schleife, aber for mit nur einer Bedingung funktioniert identisch.

    Syntax

    for bedingung {
        // Code
    }

    Für diesen Fall (while Ersatz) wird in der Regel ein Wert außerhalb der Schleife definiert.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        i := 0
        for i < 5 {
            fmt.Println("i=", i)
            i++
        }
    }
    i= 0
    i= 1
    i= 2
    i= 3
    i= 4

    Endlos Schleife

    Eine Schleife ohne Bedingung läuft unendlich (bis break oder return).

    Syntax

    for {
        // Code läuft unendlich
    }

    In der Regel wird so eine Art von Code bei Event-basierten Applikationen benötigt, wie beispielsweise Desktop-Apps, die in einer Dauerschleife auf Events reagieren müssen.

    Auch für Server-Loops (z.B. HTTP-Server) oder Worker-Prozesse werden diese Schleifen eingesetzt.

    Beispiel
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        count := 0
        for {
            fmt.Println("Läuft ...", count)
            count++
    
            if count >= 3 {
                break
            }
    
            time.Sleep(1 * time.Second)
        }
    
        fmt.Println("Schleife beendet")
    }
    Läuft ... 0
    Läuft ... 1
    Läuft ... 2
    Schleife beendet

    range - Iteration über Collections

    range ist ein spezielles Schlüsselwort, um über Arrays, Slices, Maps, Strings und Channels zu iterieren.

    range über Slices und Arrays

    Syntax

    for index, value := range collection {
        // index = Index des Elements
        // value = Wert des Elements
    }

    Sowohl Index als auch Wert können einzeln weggelassen (durch _) werden, wenn einer dieser Werte nicht benötigt wird.

    Werfen wir einen Blick auf ein grundlegendes Beispiel.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        nums := []int{10, 20, 30, 40, 50}
    
        for index, value := range nums {
            fmt.Printf("Index: %d | Wert: %d\n", index, value)
        }
    }
    Index: 0 | Wert: 10
    Index: 1 | Wert: 20
    Index: 2 | Wert: 30
    Index: 3 | Wert: 40
    Index: 4 | Wert: 50

    Würden wir beispielsweise nur Indexe benötigen, könnten wir die Variable für den Wert einfach weglassen.

    for index := range nums {
        fmt.Println("Index:", index)
    }

    Wenn wir allerdings nur den Wert, ohne Index, benötigen, müssen wir die Index-Variable durch _ (Unterstrich-Platzhalter) ersetzen.

    for _, value := range nums {
        fmt.Println("Wert:", value)
    }

    Wichtig: range gibt Kopien der Werte zurück, keine Referenzen. Änderungen an value ändern nicht das Original-Slice.

    Somit kann oft folgender Fehler passieren, wenn man versucht Werte in einem Array/Slice zu ändern.

    Beispiel - ❌ Falsch
    package main
    
    import "fmt"
    
    func main() {
        nums := []int{1, 2, 3}
    
        for _, value := range nums {
            value = value * 2
        }
    
        fmt.Println(nums)
    }
    [1 2 3]

    Wie wir sehen können, wurden die Elemente im Slice nicht aktualisiert.

    Um die Werte wirklich im ursprünglichen Slice zu aktualisieren, müsste man in diesem Fall mit Indexen arbeiten.

    Beispiel - ✅ Korrekt
    package main
    
    import "fmt"
    
    func main() {
        nums := []int{1, 2, 3}
    
        for i := range nums {
            nums[i] = nums[i] * 2
        }
    
        fmt.Println(nums)
    }
    [2 4 6]

    range über Maps

    Bei Maps liefert range den Schlüssel und den Wert.

    Syntax

    for key, value := range map {
        // key = Schlüssel
        // value = Wert
    }

    Schauen wir uns direkt ein Beispiel an. Wir erstellen uns eine Beispiel-Map.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        ages := map[string]int{
            "John": 40,
            "Alice": 30,
            "Tom": 28,
        }
    
        for name, age := range ages {
            fmt.Printf("%s ist %d Jahre alt\n", name, age)
        }
    }
    Tom ist 28 Jahre alt
    John ist 40 Jahre alt
    Alice ist 30 Jahre alt

    Wichtig

    Die Iteration-Reihenfolge bei Maps ist NICHT garantiert.

    Maps sind ungeordnet in Go. Die Reihenfolge der Iteration kann sich bei jedem Programmlauf ändern (ist sogar absichtlich randomisiert seit Go 1).

    Analog zur Verwendung von range mit Arrays/Slices, können wir auch hier nur eine Komponente verwenden.

    Nur Schlüssel benötigt

    for name := range ages {
        fmt.Println("Name:", name)
    }

    Nur Werte benötigt

    for _, age := range ages {
        fmt.Println("Alter:", age)
    }

    range über Strings

    Bei Strings iteriert range über Runes (Unicode-Codepoints), nicht Bytes.

    Syntax

    for index, rune := range string {
        // index = Byte-Position (nicht Zeichen-Position)
        // rune = Unicode-Codepoint
    }

    Hier ein Beispiel.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        text := "Hallo, 世界"
    
        for index, runeValue := range text {
            fmt.Printf("Index %d: %c (Unicode: %U)\n", index, runeValue, runeValue)
        }
    }
    Index 0: H (Unicode: U+0048)
    Index 1: a (Unicode: U+0061)
    Index 2: l (Unicode: U+006C)
    Index 3: l (Unicode: U+006C)
    Index 4: o (Unicode: U+006F)
    Index 5: , (Unicode: U+002C)
    Index 6:   (Unicode: U+0020)
    Index 7: 世 (Unicode: U+4E16)
    Index 10: 界 (Unicode: U+754C)

    Nochmals zusammengefasst

    • index ist die Byte-Position (nicht die Zeichen-Position)
    • startet bei Index 7, bei Index 10, weil 3 Bytes benötigt
    • runeValue ist vom Typ rune (alias für int32)

    Warum ist es wichtig zu beachten? Strings in Go sind UTF-8 kodiert. Ein einzelnes Unicode-Zeichen kann mehrere Bytes belegen.

    range über Channels

    Bei Channels iteriert range, bis der Channel geschlossen wird.

    Syntax

    for value := range channel {
        // Empfängt Werte bis Channel geschlossen wird
    }

    Verdeutlichen wir das Ganze an einem einfachen Beispiel.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 5)
    
        // Sender
        go func() {
            for i := 1; i <= 5; i++ {
                ch <- 1
            }
            close(ch)
        }()
    
        // Empfänger
        for value := range ch {
            fmt.Println("Empfangen:", value)
        }
    
        fmt.Println("Channel geschlossen")
    }
    Empfangen: 1
    Empfangen: 1
    Empfangen: 1
    Empfangen: 1
    Empfangen: 1
    Channel geschlossen

    Die Schleife läuft, bis der Channel mit close() geschlossen wird. Wenn der Channel nie geschlossen wird, läuft die Schleife ewig (Deadlock).

    Kontrollfluss-Anweisungen

    break - Schleife verlassen

    break beendet die innerste Schleife oder den switch Block sofort.

    Beispiel in Schleife

    package main
    
    import "fmt"
    
    func main() {
        for i := 0; i < 10; i++ {
            if i == 5 {
                break
            }
    
            fmt.Println("i =", i)
        }
    
        fmt.Println("Schleife beendet")
    }
    i = 0
    i = 1
    i = 2
    i = 3
    i = 4
    Schleife beendet

    Beispiel in verschachtelten Schleifen

    break verlässt nur die innerste Schleife.

    package main
    
    import "fmt"
    
    func main() {
        for i := 0; i < 3; i++ {
            for j := 0; j < 3; j++ {
                if j == 1 {
                    break
                }
    
                fmt.Printf("i=%d, j=%d\n", i, j)
            }
        }
    }
    i=0, j=0
    i=1, j=0
    i=2, j=0

    In diesem Beispiel sehen wir, dass die Schleife in dem Moment verlassen wird, in dem j den Wert 1 angenommen hat. Folglich kriegen wir in der Ausgabe immer j = 0. Dabei läuft die äußere Schleife weiter.


    continue - Zur nächsten Iteration

    continue überspringt den Rest der aktuellen Iteration und springt zur nächsten.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        for i := 0; i < 10; i++ {
            if i % 2 == 0 {
                continue
            }
    
            fmt.Println("Ungerade:", i)
        }
    }
    Ungerade: 1
    Ungerade: 3
    Ungerade: 5
    Ungerade: 7
    Ungerade: 9

    Hier wird bei i mit einem geraden Wert continue ausgeführt. Der Rest des Schleifenkörpers wird übersprungen und die Schleife springt zur nächsten Iteration.


    Labels - benannte Kontrollfluss-Ziele

    Labels erlaubt es, break und continue auf äußere Schleifen anzuwenden.

    Syntax

    LabelName:
    for ... {
        for ... {
            break LabelName // Verlässt die äußere Schleife
        }
    }

    Schauen wir uns dies an einem Beispiel an.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        outer:
        for i := 0; i < 3; i++ {
            for j := 0; j < 3; j++ {
                fmt.Println("i=%d, j=%d\n", i, j)
    
                if i == 1 && j == 1 {
                    break outer
                }
            }
        }
    
        fmt.Println("Fertig")
    }
    i=%d, j=%d
    0 0
    i=%d, j=%d
    0 1
    i=%d, j=%d
    0 2
    i=%d, j=%d
    1 0
    i=%d, j=%d
    1 1
    Fertig

    Ebenfalls kann man continue mit Label verwenden. Dabei springt die Schleifenausführung an den angegebenen Label-Namen.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        outer:
        for i := 0; i < 3; i++ {
            for j := 0; j < 3; j++ {
                if j == 1 {
                    continue outer
                }
    
                fmt.Printf("i=%d, j=%d\n", i, j)
            }
        }
    }
    i=0, j=0
    i=1, j=0
    i=2, j=0

    Was gibt es bei Labels zu beachten?

    • Labels müssen mit einem Doppelpunkt enden (outer:)
    • Label-Namen folgen denselben Regeln wie Variablennamen

    goto - Unbedingter Sprung

    goto springt zu einem Label im selben Funktionskörper.

    Syntax

    goto LabelName
    
    LabelName:
        // Code hier

    Ein Beispiel zur Verdeutlichung.

    Beispiel
    package main
    
    import "fmt"
    
    func main() {
        i := 0
    
    loop:
        fmt.Println("i =", i)
        i++
    
        if i < 5 {
            goto loop
        }
    
        fmt.Println("Fertig")
    }
    i = 0
    i = 1
    i = 2
    i = 3
    i = 4
    Fertig

    Die Verwendung von goto sollte nach Möglichkeit vermieden werden. Generell ist es ziemlich cool, dass Go diese Funktionalität unterstützt. Allerdings kann der Code mit goto schnell schwerer lesbar werden.