Kapitel 4. Blöcke, Schatten und Kontrollstrukturen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Nachdem ich nun Variablen, Konstanten und eingebaute Typen behandelt habe, bist du bereit, dir die Programmierlogik und die Organisation anzusehen. Zunächst erkläre ich Blöcke und wie sie steuern, wann ein Bezeichner verfügbar ist. Dann stelle ich die Kontrollstrukturen von Go vor: if, for, und switch. Zum Schluss spreche ich über goto und die eine Situation, in der du es verwenden solltest.

Blöcke

In Go kannst du Variablen an vielen Stellen deklarieren. Du kannst sie außerhalb von Funktionen, als Parameter von Funktionen und als lokale Variablen innerhalb von Funktionen deklarieren.

Hinweis

Bis jetzt hast du nur die Funktion main geschrieben, aber im nächsten Kapitel wirst du Funktionen mit Parametern schreiben.

Jeder Ort, an dem eine Deklaration vorkommt, wird als Block bezeichnet. Variablen, Konstanten, Typen und Funktionen, die außerhalb von Funktionen deklariert werden, befinden sich im Paketblock. Du hast import Anweisungen in deinen Programmen verwendet, um Zugang zu Druck- und Mathematikfunktionen zu erhalten (und ich werde in Kapitel 10 ausführlich über sie sprechen). Sie definieren Namen für andere Pakete, die für die Datei gültig sind, die die import Anweisung enthält. Diese Namen stehen in dem Dateiblock. Alle Variablen, die auf der obersten Ebene einer Funktion definiert sind (einschließlich der Parameter für eine Funktion), befinden sich in einem Block. Innerhalb einer Funktion definiert jeder Satz geschweifter Klammern ({}) einen weiteren Block, und gleich wirst du sehen, dass die Kontrollstrukturen in Go eigene Blöcke definieren.

Du kannst auf einen Bezeichner, der in einem äußeren Block definiert ist, von jedem inneren Block aus zugreifen. Das wirft die Frage auf: Was passiert, wenn du eine Deklaration mit dem gleichen Namen wie ein Bezeichner in einem enthaltenen Block hast? Wenn du das tust, überschattest du den Bezeichner, der im äußeren Block erstellt wurde.

Beschattungsvariablen

Bevor ich erkläre, was Shadowing ist, lass uns einen Blick auf den Code werfen (siehe Beispiel 4-1). Du kannst ihn auf dem Go Playground oder im Verzeichnis sample_code/shadow_variables im Repository von Kapitel 4 ausführen.

Beispiel 4-1. Beschattung von Variablen
func main() {
    x := 10
    if x > 5 {
        fmt.Println(x)
        x := 5
        fmt.Println(x)
    }
    fmt.Println(x)
}

Bevor du diesen Code ausführst, versuche zu erraten, was er ausgeben wird:

  • Es wird nichts gedruckt; der Code lässt sich nicht kompilieren

  • 10 auf Linie eins, 5 auf Linie zwei, 5 auf Linie drei

  • 10 auf Linie eins, 5 auf Linie zwei, 10 auf Linie drei

Und so geht's:

10
5
10

Eine Shadowing-Variable ist eine Variable, die denselben Namen hat wie eine Variable in einem enthaltenen Block. Solange die Shadowing-Variable existiert, kannst du nicht auf eine Shadowing-Variable zugreifen.

In diesem Fall wolltest du mit ziemlicher Sicherheit keine brandneue x innerhalb der if Anweisung erstellen. Stattdessen wolltest du wahrscheinlich dem x, das auf der obersten Ebene des Funktionsblocks deklariert wurde, 5 zuweisen. In der ersten fmt.Println innerhalb der if Anweisung kannst du auf die x zugreifen, die auf der obersten Ebene der Funktion deklariert ist. In der nächsten Zeile beschattest du jedoch x, indem du eine neue Variable mit demselben Namen innerhalb des Blocks deklarierst, der durch den Körper der if Anweisung erstellt wurde. Wenn du in der zweiten Anweisung fmt.Println auf die Variable x zugreifst, erhältst du die Shadowing-Variable, die den Wert 5 hat. Die schließende Klammer für den Körper der Anweisung if beendet den Block, in dem die Shadowing-Variable x existiert. Wenn du in der dritten fmt.Println auf die Variable x zugreifst, erhältst du die auf der obersten Ebene der Funktion deklarierte Variable, die den Wert 10 hat. Beachte, dass diese x nicht verschwunden ist oder neu zugewiesen wurde; es gab nur keine Möglichkeit, auf sie zuzugreifen, nachdem sie im inneren Block beschattet wurde.

Im vorigen Kapitel habe ich erwähnt, dass ich in manchen Situationen die Verwendung von := vermeide, weil dadurch unklar werden kann, welche Variablen verwendet werden. Das liegt daran, dass es sehr einfach ist, eine Variable versehentlich zu überschatten, wenn du := verwendest. Erinnere dich daran, dass du mit := mehrere Variablen auf einmal erstellen und zuweisen kannst. Außerdem müssen nicht alle Variablen auf der linken Seite neu sein, damit := legal ist. Du kannst := verwenden, solange es mindestens eine neue Variable auf der linken Seite gibt. Schauen wir uns ein anderes Programm an (siehe Beispiel 4-2), das du auf The Go Playground oder im Verzeichnis sample_code/shadow_multiple_assignment im Repository von Kapitel 4 findest.

Beispiel 4-2. Beschattung mit Mehrfachzuweisung
func main() {
    x := 10
    if x > 5 {
        x, y := 5, 20
        fmt.Println(x, y)
    }
    fmt.Println(x)
}

Wenn du diesen Code ausführst, erhältst du folgendes Ergebnis:

5 20
10

Obwohl es eine bestehende Definition von x in einem äußeren Block gab, wurde x trotzdem in der Anweisung if überschattet. Das liegt daran, dass := nur Variablen wiederverwendet, die im aktuellen Block deklariert sind. Wenn du := verwendest, achte darauf, dass du keine Variablen aus einem äußeren Bereich auf der linken Seite hast, es sei denn, du willst sie schattieren.

Du musst auch darauf achten, dass du mit keinen Paketimport beschattest. Ich werde in Kapitel 10 mehr über den Import von Paketen erzählen, aber du hast das Paket fmt importiert, um die Ergebnisse unserer Programme auszudrucken. Schauen wir uns an, was passiert, wenn du in deiner Funktion main eine Variable namens fmt deklarierst, wie in Beispiel 4-3 gezeigt. Du kannst versuchen, es auf dem Go Playground oder im Verzeichnis sample_code/shadow_package_names im Repository von Kapitel 4 auszuführen.

Beispiel 4-3. Schattierung von Paketnamen
func main() {
    x := 10
    fmt.Println(x)
    fmt := "oops"
    fmt.Println(fmt)
}

Wenn du versuchst, diesen Code auszuführen, bekommst du eine Fehlermeldung:

fmt.Println undefined (type string has no field or method Println)

Das Problem ist nicht, dass du deine Variable fmt genannt hast, sondern dass du versucht hast, auf etwas zuzugreifen, das die lokale Variable fmt nicht hat. Sobald die lokale Variable fmt deklariert ist, überschattet sie das Paket mit dem Namen fmt im Dateiblock und macht es unmöglich, das Paket fmt für den Rest der Funktion main zu verwenden.

Da das Shadowing in einigen wenigen Fällen nützlich ist (in späteren Kapiteln werden wir darauf eingehen), meldet go vet es nicht als wahrscheinlichen Fehler. In "Verwenden von Code-Qualitäts-Scannern" lernst du Tools von Drittanbietern kennen, die versehentliches Shadowing in deinem Code erkennen können.

wenn

Die Anweisung if in Go ist ähnlich wie die Anweisung if in den meisten Programmiersprachen. Weil sie ein so vertrautes Konstrukt ist, habe ich sie schon in früheren Beispielen verwendet, ohne mir Sorgen zu machen, dass sie verwirrend sein könnte. Beispiel 4-5 zeigt ein vollständigeres Beispiel.

Beispiel 4-5. if und else
n := rand.Intn(10)
if n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}

Der sichtbarste Unterschied zwischen if Anweisungen in Go und anderen Sprachen ist, dass du keine Klammern um die Bedingung setzt. Aber Go fügt den if Anweisungen eine weitere Funktion hinzu, mit der du deine Variablen besser verwalten kannst.

Wie ich in "Shadowing-Variablen" erläutert habe , existiert jede Variable, die innerhalb der geschweiften Klammern einer if oder else Anweisung deklariert wird, nur innerhalb dieses Blocks. Das ist nicht ungewöhnlich und gilt für die meisten Sprachen. Go bietet die Möglichkeit, Variablen zu deklarieren, die sowohl für die Bedingung als auch für die Blöcke if und else gültig sind. Schau dir Beispiel 4-6 an, das unser vorheriges Beispiel umschreibt, um diesen Bereich zu nutzen.

Beispiel 4-6. Scoping einer Variablen auf eine if Anweisung
if n := rand.Intn(10); n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}

Dieser spezielle Bereich ist sehr praktisch. Damit kannst du Variablen erstellen, die nur dort verfügbar sind, wo sie gebraucht werden. Sobald die Reihe der if/else Anweisungen endet, ist n undefiniert. Du kannst dies testen, indem du versuchst, den Code in Beispiel 4-7 auf dem Go Playground oder im Verzeichnis sample_code/if_bad_scope im Repository von Kapitel 4 auszuführen.

Beispiel 4-7. Außerhalb des Bereichs...
if n := rand.Intn(10); n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}
fmt.Println(n)

Der Versuch, diesen Code auszuführen, führt zu einem Kompilierungsfehler:

undefined: n
Hinweis

Technisch gesehen kannst du jede einfache Anweisung vor den Vergleich in einer if Anweisung setzen - auch einen Funktionsaufruf, der keinen Wert zurückgibt, oder eine Zuweisung eines neuen Wertes an eine bestehende Variable. Aber tu das nicht. Verwende diese Funktion nur, um neue Variablen zu definieren, die auf die if/else Anweisungen beschränkt sind; alles andere wäre verwirrend.

Beachte auch, dass eine Variable, die als Teil einer if Anweisung deklariert wird, wie jeder andere Block auch, Variablen mit demselben Namen, die in den enthaltenen Blöcken deklariert sind, überschattet.

für, Vier Wege

Wie in anderen Sprachen der C-Familie verwendet Go eine for Anweisung für eine Schleife. Was Go von anderen Sprachen unterscheidet, ist, dass for das einzige Schlüsselwort für Schleifen in der Sprache ist. Go erreicht dies, indem es das Schlüsselwort for in vier Formaten verwendet:

  • Eine vollständige, im C-Stil for

  • Eine Nur-Bedingung for

  • Eine unendliche for

  • for-range

Das vollständige Formblatt

Der erste for Schleifenstil ist die vollständige for Deklaration, die du vielleicht aus C, Java oder JavaScript kennst, wie in Beispiel 4-8 gezeigt.

Beispiel 4-8. Eine vollständige for Anweisung
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

Es wird dich nicht überraschen, dass dieses Programm die Zahlen von 0 bis einschließlich 9 ausgibt.

Genau wie die Anweisung if verwendet die Anweisung for keine Klammern um ihre Teile. Ansonsten sollte sie vertraut aussehen. Die Anweisung if besteht aus drei Teilen, die durch Semikolons getrennt sind. Der erste Teil ist eine Initialisierung, die eine oder mehrere Variablen setzt, bevor die Schleife beginnt. Du solltest dich an zwei wichtige Details des Initialisierungsteils erinnern. Erstens musst du := verwenden, um die Variablen zu initialisieren; var ist hier nicht zulässig. Zweitens kannst du hier, genau wie bei den Variablendeklarationen in den if Anweisungen, eine Variable schattieren.

Der zweite Teil ist der Vergleich. Dies muss ein Ausdruck sein, der als bool ausgewertet wird. Er wird unmittelbar vor jeder Iteration der Schleife überprüft. Wenn der Ausdruck den Wert true ergibt, wird der Schleifenkörper ausgeführt.

Der letzte Teil der Standardanweisung for ist das Inkrement. Normalerweise siehst du hier etwas wie i{plus}{plus}, aber jede Zuweisung ist gültig. Sie wird unmittelbar nach jeder Iteration der Schleife ausgeführt, bevor die Bedingung ausgewertet wird.

Go erlaubt es dir, einen oder mehrere der drei Teile der for Anweisung wegzulassen. Meistens lässt du entweder die Initialisierung weg, wenn sie auf einem Wert basiert, der vor der Schleife berechnet wurde:

i := 0
for ; i < 10; i++ {
    fmt.Println(i)
}

oder du lässt das Inkrement weg, weil du eine kompliziertere Inkrementregel innerhalb der Schleife hast:

for i := 0; i < 10; {
    fmt.Println(i)
    if i % 2 == 0 {
        i++
    } else {
        i+=2
    }
}

Die Bedingung - nur für Aussage

Wenn du sowohl die Initialisierung als auch das Inkrement in einer for Anweisung weglässt, darfst du die Semikolons nicht einfügen. (Wenn du das tust, werden sie von go fmt entfernt.) So bleibt eine for Anweisung übrig, die wie die while Anweisung in C, Java, JavaScript, Python, Ruby und vielen anderen Sprachen funktioniert. Sie sieht aus wie in Beispiel 4-9.

Beispiel 4-9. Eine Nur-Bedingung-Anweisung for
i := 1
for i < 100 {
        fmt.Println(i)
        i = i * 2
}

Das Unendliche für Statement

Das dritte Anweisungsformat for macht die Bedingung ebenfalls überflüssig. Go hat eine Version der for Schleife, die eine Endlosschleife bildet. Wenn du in den 1980er Jahren programmieren gelernt hast, war dein erstes Programm wahrscheinlich eine Endlosschleife in BASIC, die HELLO für immer auf dem Bildschirm ausgab:

10 PRINT "HELLO"
20 GOTO 10

Beispiel 4-10 zeigt die Go-Version dieses Programms. Du kannst es lokal ausführen oder es auf dem Go Playground oder im Verzeichnis sample_code/infinite_for im Repository von Kapitel 4 ausprobieren.

Beispiel 4-10. Nostalgie in der Endlosschleife
package main

import "fmt"

func main() {
    for {
        fmt.Println("Hello")
    }
}

Wenn du dieses Programm ausführst, erhältst du dieselbe Ausgabe, die auch die Bildschirme von Millionen von Commodore 64s und Apple ][s füllte:

Hello
Hello
Hello
Hello
Hello
Hello
Hello
...

Drücke Strg-C, wenn du keine Lust mehr hast, in Erinnerungen zu schwelgen.

Hinweis

Wenn du Beispiel 4-10 auf dem Go Playground ausführst, wirst du feststellen, dass es nach ein paar Sekunden aufhört, ausgeführt zu werden. Als gemeinsam genutzte Ressource lässt der Playground nicht zu, dass ein einziges Programm zu lange läuft.

Pause und weiter

Wie kommst du aus einer for Endlosschleife heraus, ohne die Tastatur zu benutzen oder deinen Computer auszuschalten? Das ist die Aufgabe der Anweisung break. Sie verlässt die Schleife sofort, genau wie die Anweisung break in anderen Sprachen. Natürlich kannst du break mit jeder for Anweisung verwenden, nicht nur mit der Endlosanweisung for.

Hinweis

Go hat keine Entsprechung zu dem Schlüsselwort do in Java, C und JavaScript. Wenn du mindestens einmal iterieren willst, ist der sauberste Weg, eine for Endlosschleife zu verwenden, die mit einer if Anweisung endet. Wenn du z.B. Java-Code hast, der eine do/while Schleife verwendet:

do {
    // things to do in the loop
} while (CONDITION);

die Go-Version sieht so aus:

for {
    // things to do in the loop
    if !CONDITION {
        break
    }
}

Beachte, dass die Bedingung ein führendes ! hat, um die Bedingung aus dem Java-Code zu negieren. Der Go-Code gibt an, wie die Schleife zu verlassen ist, während der Java-Code angibt, wie man in der Schleife bleibt.

In Go gibt es auch das Schlüsselwort continue , das den Rest des for Schleifenkörpers überspringt und direkt mit der nächsten Iteration fortfährt. Technisch gesehen, brauchst du keinecontinue Anweisung. Du könntest Code wie in Beispiel 4-11 schreiben.

Beispiel 4-11. Verwirrender Code
for i := 1; i <= 100; i++ {
    if i%3 == 0 {
        if i%5 == 0 {
            fmt.Println("FizzBuzz")
        } else {
            fmt.Println("Fizz")
        }
    } else if i%5 == 0 {
        fmt.Println("Buzz")
    } else {
        fmt.Println(i)
    }
}

Aber das ist nicht idiomatisch. Go fördert kurze if Anweisungskörper, die so links wie möglich ausgerichtet sind. Verschachtelter Code ist schwieriger zu verstehen. Wenn du eine continue Anweisung verwendest, ist es einfacher zu verstehen, was vor sich geht. Beispiel 4-12 zeigt den Code aus dem vorigen Beispiel, umgeschrieben, um continue zu verwenden.

Beispiel 4-12. continue verwenden, um den Code klarer zu machen
for i := 1; i <= 100; i++ {
    if i%3 == 0 && i%5 == 0 {
        fmt.Println("FizzBuzz")
        continue
    }
    if i%3 == 0 {
        fmt.Println("Fizz")
        continue
    }
    if i%5 == 0 {
        fmt.Println("Buzz")
        continue
    }
    fmt.Println(i)
}

Wie du siehst, werden die Bedingungen aufgereiht, wenn du die Kette von if/else Anweisungen durch eine Reihe von if Anweisungen ersetzt, die continue verwenden. Das verbessert das Layout deiner Bedingungen, was bedeutet, dass dein Code leichter zu lesen und zu verstehen ist.

Die for-range-Anweisung

Das vierte for Anweisungsformat ist für die Iteration über Elemente in einigen der eingebauten Typen von Go. Sie wird for-range Schleife genannt und ähnelt den Iteratoren, die in anderen Sprachen verwendet werden. Dieser Abschnitt zeigt, wie du eine for-range Schleife mit Strings, Arrays, Slices und Maps verwendest. Wenn ich mich in Kapitel 12 mit Kanälen beschäftige, werde ich darüber sprechen, wie man sie mit for-range Schleifen verwendet.

Hinweis

Du kannst eine for-range Schleife nur verwenden, um über die eingebauten zusammengesetzten Typen und die darauf basierenden benutzerdefinierten Typen zu iterieren.

Als erstes wollen wir uns ansehen, wie man eine for-range Schleife mit einem Slice verwendet. Du kannst den Code in Beispiel 4-13 auf dem Go Playground oder in der Funktion forRangeKeyValue in main.go im Verzeichnis sample_code/for_range im Repository von Kapitel 4 ausprobieren.

Beispiel 4-13. Die for-range Schleife
evenVals := []int{2, 4, 6, 8, 10, 12}
for i, v := range evenVals {
    fmt.Println(i, v)
}

Wenn du diesen Code ausführst, erhältst du die folgende Ausgabe:

0 2
1 4
2 6
3 8
4 10
5 12

Das Interessante an einer for-range Schleife ist, dass du zwei Schleifenvariablen erhältst. Die erste Variable ist die Position in der Datenstruktur, die durchlaufen wird, die zweite ist der Wert an dieser Position. Die idiomatischen Namen für die beiden Schleifenvariablen hängen davon ab, was in der Schleife durchlaufen wird. Beim Durchlaufen einer Schleife über ein Array, ein Slice oder einen String wird üblicherweise i für Index verwendet. Bei der Iteration durch eine Map wird stattdessen k (for key) verwendet.

Die zweite Variable wird häufig v für Wert genannt, aber manchmal wird ihr auch ein Name gegeben, der auf dem Typ der Werte basiert, die iteriert werden. Natürlich kannst du den Variablen jeden beliebigen Namen geben. Wenn der Schleifenkörper nur wenige Anweisungen enthält, sind Variablennamen mit nur einem Buchstaben gut geeignet. Für längere (oder verschachtelte) Schleifen solltest du beschreibendere Namen verwenden.

Was ist, wenn du die erste Variable innerhalb deiner for-range Schleife nicht verwenden musst? Erinnere dich daran, dass du in Go auf alle deklarierten Variablen zugreifen musst, und diese Regel gilt auch für die Variablen, die als Teil einer for Schleife deklariert sind. Wenn du nicht auf den Schlüssel zugreifen musst, verwende einen Unterstrich (_) als Variablennamen. Dadurch wird Go angewiesen, den Wert zu ignorieren.

Schreiben wir den Code für den Slice Ranging um, damit die Position nicht ausgedruckt wird. Du kannst den Code in Beispiel 4-14 auf dem Go Playground oder in der Funktion forRangeIgnoreKey in main.go im Verzeichnis sample_code/for_range im Repository von Kapitel 4 ausführen.

Beispiel 4-14. Ignorieren des Slice-Index in einer for-range Schleife
evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    fmt.Println(v)
}

Wenn du diesen Code ausführst, erhältst du die folgende Ausgabe:

2
4
6
8
10
12
Tipp

Immer wenn du in einer Situation bist, in der ein Wert zurückgegeben wird, den du aber ignorieren möchtest, kannst du einen Unterstrich verwenden, um den Wert zu verstecken. Du wirst das Unterstrichmuster wieder sehen, wenn ich über Funktionen in Kapitel 5 und Pakete in Kapitel 10 spreche.

Was ist, wenn du den Schlüssel, aber nicht den Wert brauchst? In diesem Fall kannst du mit Go die zweite Variable einfach weglassen. Dies ist gültiger Go-Code:

uniqueNames := map[string]bool{"Fred": true, "Raul": true, "Wilma": true}
for k := range uniqueNames {
    fmt.Println(k)
}

Der häufigste Grund für die Iteration über den Schlüssel ist, wenn eine Map als Set verwendet wird. In solchen Situationen ist der Wert unwichtig. Du kannst den Wert aber auch weglassen, wenn du über Arrays oder Slices iterierst. Das ist selten, denn der übliche Grund für die Iteration über eine lineare Datenstruktur ist der Zugriff auf die Daten. Wenn du dieses Format für ein Array oder eine Slice verwendest, ist die Wahrscheinlichkeit groß, dass du die falsche Datenstruktur gewählt hast und ein Refactoring in Betracht ziehen solltest.

Hinweis

Wenn du dir die Kanäle in Kapitel 12 ansiehst, wirst du eine Situation sehen, in der eine for-range -Schleife bei jeder Iteration nur einen einzigen Wert zurückgibt.

Iterieren über Karten

Es ist interessant zu sehen, wie eine for-range Schleife über eine Map iteriert:. Du kannst den Code in Beispiel 4-15 auf The Go Playground oder im Verzeichnis sample_code/iterate_map im Kapitel 4 Repository ausführen.

Beispiel 4-15. Iterationsreihenfolge der Karte variiert
m := map[string]int{
    "a": 1,
    "c": 3,
    "b": 2,
}

for i := 0; i < 3; i++ {
    fmt.Println("Loop", i)
    for k, v := range m {
        fmt.Println(k, v)
    }
}

Wenn du dieses Programm erstellst und ausführst, variiert die Ausgabe. Hier ist eine Möglichkeit:

Loop 0
c 3
b 2
a 1
Loop 1
a 1
c 3
b 2
Loop 2
b 2
a 1
c 3

Die Reihenfolge der Schlüssel und Werte variiert; einige Durchläufe können identisch sein. Das ist eigentlich ein Sicherheitsmerkmal. In früheren Go-Versionen war die Iterationsreihenfolge der Schlüssel in einer Map normalerweise (aber nicht immer) gleich, wenn du die gleichen Elemente in eine Map eingefügt hast. Das verursachte zwei Probleme:

  • Die Leute schrieben Code, der davon ausging, dass die Reihenfolge festgelegt war, und dieser Code brach zu merkwürdigen Zeiten ab.

  • Wenn Maps immer die gleichen Hash-Werte haben und du weißt, dass ein Server Benutzerdaten in einer Map speichert, kannst du einen Server mit einem Angriff namens Hash DoS verlangsamen, indem du ihm speziell erstellte Daten mit Schlüsseln schickst, die alle den gleichen Hash-Wert haben.

Um diese beiden Probleme zu vermeiden, hat das Go-Team zwei Änderungen an der Map-Implementierung vorgenommen. Zunächst wurde der Hash-Algorithmus für Maps so geändert, dass er eine Zufallszahl enthält, die jedes Mal erzeugt wird, wenn eine Map-Variable erstellt wird. Als Nächstes wurde die Reihenfolge der for-range Iteration über eine Map bei jedem Durchlauf der Map ein wenig verändert. Diese beiden Änderungen machen es viel schwieriger, einen Hash-DoS-Angriff durchzuführen.

Hinweis

Diese Regel hat eine Ausnahme. Um das Debuggen und Protokollieren von Maps zu erleichtern, geben die Formatierungsfunktionen (wie fmt.Println) Maps immer mit ihren Schlüsseln in aufsteigender Reihenfolge aus.

Iteration über Strings

Wie ich bereits erwähnt habe, kannst du auch einen String mit einer for-range Schleife verwenden. Schauen wir uns das mal an. Du kannst den Code in Beispiel 4-16 auf deinem Computer oder auf The Go Playground oder im Verzeichnis sample_code/iterate_string im Repository von Kapitel 4 ausführen.

Beispiel 4-16. Iteration über Strings
samples := []string{"hello", "apple_π!"}
for _, sample := range samples {
    for i, r := range sample {
        fmt.Println(i, r, string(r))
    }
    fmt.Println()
}

Die Ausgabe, wenn der Code über das Wort "Hallo" iteriert, bietet keine Überraschungen:

0 104 h
1 101 e
2 108 l
3 108 l
4 111 o

In der ersten Spalte steht der Index, in der zweiten der numerische Wert des Buchstabens und in der dritten der numerische Wert des in eine Zeichenkette umgewandelten Buchstabentyps.

Interessanter ist es, das Ergebnis für "apple_π!" zu betrachten:

0 97 a
1 112 p
2 112 p
3 108 l
4 101 e
5 95 _
6 960 π
8 33 !

Beachte zwei Dinge bei dieser Ausgabe. Erstens: In der ersten Spalte wird die Zahl 7 übersprungen. Zweitens: Der Wert an Position 6 ist 960. Das ist viel größer als das, was in ein Byte passen kann. Aber in Kapitel 3 hast du gesehen, dass Strings aus Bytes bestehen. Was ist hier los?

Was du siehst, ist ein spezielles Verhalten von bei der Iteration über eine Zeichenkette mit einer for-range Schleife. Die Schleife durchläuft die Runen, nicht die Bytes. Wann immer eine for-range Schleife auf eine Multibyte-Rune in einem String stößt, wandelt sie die UTF-8-Darstellung in eine einzelne 32-Bit-Zahl um und weist sie dem Wert zu. Der Offset wird um die Anzahl der Bytes in der Rune erhöht. Wenn die Schleife for-range auf ein Byte stößt, das keinen gültigen UTF-8-Wert darstellt, wird stattdessen das Unicode-Ersatzzeichen (Hex-Wert 0xfffd) zurückgegeben.

Tipp

Verwende eine for-range Schleife, um die Runen in einem String der Reihe nach aufzurufen. Die erste Variable enthält die Anzahl der Bytes vom Anfang der Zeichenkette, aber der Typ der zweiten Variable ist die Rune.

Der for-range-Wert ist eine Kopie

Du solltest dir bewusst sein, dass die for-range Schleife jedes Mal, wenn sie über deinen zusammengesetzten Typ iteriert, den Wert aus dem zusammengesetzten Typ in die Wertvariable kopiert. Wenn du die Wertvariable änderst, wird der Wert im zusammengesetzten Typ nicht geändert. Beispiel 4-17 zeigt ein schnelles Programm, um dies zu demonstrieren. Du kannst es auf dem Go Playground oder in der Funktion forRangeIsACopy in main.go im Verzeichnis sample_code/for_range im Repository von Kapitel 4 ausprobieren.

Beispiel 4-17. Das Ändern des Wertes ändert nicht die Quelle
evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    v *= 2
}
fmt.Println(evenVals)

Wenn du diesen Code ausführst, erhältst du die folgende Ausgabe:

[2 4 6 8 10 12]

In Go-Versionen vor 1.22 wird die Wertvariable einmal erstellt und bei jeder Iteration durch die Schleife for wiederverwendet. Seit Go 1.22 wird standardmäßig bei jedem Durchlauf der for Schleife eine neue Index- und Wertvariable erstellt. Diese Änderung mag nicht wichtig erscheinen, aber sie verhindert einen häufigen Fehler. Wenn ich in "Goroutinen, for-Schleifen und variierende Variablen" über Goroutinen und for-range -Schleifen spreche , wirst du sehen, dass du vor Go 1.22, wenn du Goroutinen in einer for-range -Schleife gestartet hast, darauf achten musstest, wie du den Index und den Wert an die Goroutinen übergibst, sonst würdest du überraschend falsche Ergebnisse erhalten.

Da es sich hierbei um eine rückwärtsgewandte Änderung handelt (auch wenn es eine Änderung ist, die einen häufigen Fehler beseitigt), kannst du steuern, ob dieses Verhalten aktiviert werden soll, indem du die Go-Version in der go Direktive in der go.mod Datei für dein Modul angibst. Mehr dazu erfährst du unter "go.mod verwenden".

Genau wie bei den anderen drei Formen der for Anweisung kannst du break und continue mit einer for-range Schleife verwenden.

Beschriftung deiner For-Aussagen

Die Schlüsselwörter break und continue gelten standardmäßig für die for Schleife, die sie direkt enthält. Was ist, wenn du geschachtelte for Schleifen hast und einen Iterator einer äußeren Schleife verlassen oder überspringen willst? Schauen wir uns ein Beispiel an. Du wirst das frühere Programm zur Iteration von Zeichenketten so abändern, dass die Iteration einer Zeichenkette aufhört, sobald sie auf den Buchstaben "l" trifft. Du kannst den Code in Beispiel 4-18 auf dem Go Playground oder im Verzeichnis sample_code/for_label im Repository von Kapitel 4 ausführen.

Beispiel 4-18. Etiketten
func main() {
    samples := []string{"hello", "apple_π!"}
outer:
    for _, sample := range samples {
        for i, r := range sample {
            fmt.Println(i, r, string(r))
            if r == 'l' {
                continue outer
            }
        }
        fmt.Println()
    }
}

Beachte, dass die Beschriftung outer durch go fmt auf die gleiche Ebene wie die umgebende Funktion eingerückt ist. Beschriftungen werden immer auf der gleichen Ebene eingerückt wie die geschweiften Klammern des Blocks. So sind sie leichter zu erkennen. Wenn du das Programm ausführst, erhältst du die folgende Ausgabe:

0 104 h
1 101 e
2 108 l
0 97 a
1 112 p
2 112 p
3 108 l

Verschachtelte for Schleifen mit Labels sind selten. Am häufigsten werden sie verwendet, um Algorithmen ähnlich dem folgenden Pseudocode zu implementieren:

outer:
    for _, outerVal := range outerValues {
        for _, innerVal := range outerVal {
            // process innerVal
            if invalidSituation(innerVal) {
                continue outer
            }
        }
        // here we have code that runs only when all of the
        // innerVal values were successfully processed
    }

Das Richtige für die Aussage wählen

Nachdem ich nun alle Formen der for Anweisung behandelt habe, fragst du dich vielleicht, wann du welches Format verwenden solltest. In den meisten Fällen wirst du das Format for-range verwenden. Eine for-range -Schleife ist der beste Weg, um eine Zeichenkette zu durchlaufen, denn sie gibt dir korrekt Runen statt Bytes zurück. Du hast auch gesehen, dass eine for-range Schleife gut funktioniert, um durch Slices und Maps zu iterieren, und du wirst in Kapitel 12 sehen, dass Kanäle auch mit for-range funktionieren.

Tipp

Bevorzuge eine for-range Schleife, wenn du über den gesamten Inhalt einer Instanz eines der eingebauten zusammengesetzten Typen iterierst. Sie vermeidet eine große Menge an Code, der erforderlich ist, wenn du ein Array, ein Slice oder eine Map mit einem der anderen for Schleifenstile verwendest.

Wann solltest du die vollständige for Schleife verwenden? Am besten verwendest du sie, wenn du nicht vom ersten bis zum letzten Element eines zusammengesetzten Typs iterierst. Du könntest zwar eine Kombination aus if, continue und break in einer for-range Schleife verwenden, aber eine standardmäßige for Schleife ist ein klarerer Weg, um den Beginn und das Ende deiner Iteration anzuzeigen. Vergleiche diese beiden Codeschnipsel, die beide über das zweite bis vorletzte Element in einem Array iterieren. Zuerst die for-range Schleife:

evenVals := []int{2, 4, 6, 8, 10}
for i, v := range evenVals {
    if i == 0 {
        continue
    }
    if i == len(evenVals)-1 {
        break
    }
    fmt.Println(i, v)
}

Und hier ist derselbe Code, mit einer Standardschleife for:

evenVals := []int{2, 4, 6, 8, 10}
for i := 1; i < len(evenVals)-1; i++ {
    fmt.Println(i, evenVals[i])
}

Der Standardcode der for Schleife ist sowohl kürzer als auch einfacher zu verstehen.

Warnung

Dieses Muster funktioniert nicht, wenn du den Anfang einer Zeichenkette überspringst. Erinnere dich daran, dass die Standardschleife for nicht richtig mit Multibyte-Zeichen umgehen kann. Wenn du einige Runen in einer Zeichenkette überspringen willst, musst du eine for-range Schleife verwenden, damit sie die Runen richtig verarbeitet.

Die beiden anderen for Anweisungsformate werden weniger häufig verwendet. Die reine Bedingungsschleife for ist, wie die while Schleife, die sie ersetzt, nützlich, wenn du eine Schleife auf der Grundlage eines berechneten Wertes durchführst.

Die Endlosschleife for ist in manchen Situationen nützlich. Der Körper der for Schleife sollte immer eine break oder return Schleife enthalten, weil du selten eine Endlosschleife machen willst. Echte Programme sollten die Iteration begrenzen und gnädig fehlschlagen, wenn Operationen nicht abgeschlossen werden können. Wie bereits gezeigt, kann eine for Endlosschleife mit einer if Anweisung kombiniert werden, um die do Anweisung zu simulieren, die es in anderen Sprachen gibt. Eine for Endlosschleife wird auch verwendet, um einige Versionen des Iteratormusters zu implementieren, das du bei der Besprechung der Standardbibliothek in "io and Friends" kennenlernen wirst.

switch

Wie viele von C abgeleitete Sprachen hat auch Go eine switch Anweisung. Die meisten Entwicklerinnen und Entwickler in diesen Sprachen vermeiden switch Anweisungen, weil sie die Werte, die eingeschaltet werden können, einschränken und das standardmäßige Fall-Through-Verhalten. Aber Go ist anders. Es macht switch Anweisungen nützlich.

Hinweis

Für die Leser, die mit Go besser vertraut sind, werde ich in diesem Kapitel die Expression-Switch-Anweisungen behandeln. Auf Typ-Switch-Anweisungen gehe ich ein, wenn ich in Kapitel 7 über Schnittstellen spreche.

Auf den ersten Blick sehen die switch Anweisungen in Go nicht viel anders aus als in C/C++, Java oder JavaScript, aber es gibt ein paar Überraschungen. Werfen wir einen Blickauf ein Beispiel füreine switch Anweisung. Du kannst den Code in Beispiel 4-19 auf The Go Playground oder in der Funktion basicSwitch in main.go im Verzeichnis sample_code/switch im Repository von Kapitel 4 ausführen.

Beispiel 4-19. Die Anweisung switch
words := []string{"a", "cow", "smile", "gopher",
    "octopus", "anthropologist"}
for _, word := range words {
    switch size := len(word); size {
    case 1, 2, 3, 4:
        fmt.Println(word, "is a short word!")
    case 5:
        wordLen := len(word)
        fmt.Println(word, "is exactly the right length:", wordLen)
    case 6, 7, 8, 9:
    default:
        fmt.Println(word, "is a long word!")
    }
}

Wenn du diesen Code ausführst, erhältst du die folgende Ausgabe:

a is a short word!
cow is a short word!
smile is exactly the right length: 5
anthropologist is a long word!

Ich gehe auf die Funktionen der switch Anweisung ein, um die Ausgabe zu erklären. Wie bei der Anweisung if setzt du auch bei der Anweisung switch keine Klammern um den zu vergleichenden Wert. Wie bei der Anweisung if kannst du eine Variable deklarieren, die auf alle Zweige der Anweisung switch anwendbar ist. In diesem Fall deklarierst du die Variable size für alle Fälle der switch Anweisung.

Alle case -Klauseln (und die optionale default -Klausel) befinden sich innerhalb einer Reihe von geschweiften Klammern. Du solltest jedoch beachten, dass du keine geschweiften Klammern um den Inhalt der case Klauseln setzt. Du kannst mehrere Zeilen innerhalb einer case (oder default)-Klausel haben, und sie werden alle als Teil desselben Blocks betrachtet.

Innerhalb von case 5: deklarierst du wordLen, eine neue Variable. Da es sich um einen neuen Block handelt, kannst du innerhalb dieses Blocks neue Variablen deklarieren. Wie bei jedem anderen Block auch, sind alle Variablen, die innerhalb des Blocks einer case Klausel deklariert werden, nur innerhalb dieses Blocks sichtbar.

Wenn du daran gewöhnt bist, am Ende jeder case -Anweisung eine break -Anweisung in deine switch -Anweisungen einzufügen, wirst du dich freuen, dass es sie nicht mehr gibt. In der Standardeinstellung werden Fälle in switch Anweisungen in Go nicht durchlaufen. Das entspricht eher dem Verhalten in Ruby oder (wenn du ein Programmierer der alten Schule bist) Pascal.

Das wirft die Frage auf: Wenn die Fälle nicht durchfallen, was machst du dann, wenn es mehrere Werte gibt, die genau dieselbe Logik auslösen sollen? In Go trennst du mehrere Übereinstimmungen mit Kommas, so wie du es bei den Übereinstimmungen 1, 2, 3 und 4 oder 6, 7, 8 und 9 machst. Deshalb erhältst du für a und cow die gleiche Ausgabe.

Das führt zur nächsten Frage: Was passiert, wenn du kein Fall-through hast und ein leerer Fall vorliegt (wie im Beispielprogramm, wenn die Länge deines Arguments 6, 7, 8 oder 9 Zeichen beträgt)? In Go bedeutet ein leerer Fall, dass nichts passiert. Deshalb siehst du auch keine Ausgabe von deinem Programm, wenn du Octopus oder Gopher als Parameter verwendest.

Tipp

Der Vollständigkeit halber: Go enthält das Schlüsselwort fallthrough, mit dem ein Fall in den nächsten übergeht. Bitte überlege es dir zweimal, bevor du einen Algorithmus implementierst, der dieses Schlüsselwort verwendet. Wenn du fallthrough verwenden musst, versuche, deine Logik so umzustrukturieren, dass die Abhängigkeiten zwischen den Fällen aufgehoben werden.

Im Beispielprogramm schaltest du auf den Wert einer Ganzzahl, aber das ist nicht alles, was du tun kannst. Du kannst jeden Typ einschalten, der mit == verglichen werden kann. Dazu gehören alle eingebauten Typen außer Slices, Maps, Channels, Funktionen und Structs, die Felder dieser Typen enthalten.

Auch wenn du nicht eine break Anweisung an das Ende jeder case Klausel setzen musst, kannst du sie verwenden, wenn du eine case Klausel vorzeitig verlassen willst. Die Notwendigkeit einer break Anweisung könnte jedoch darauf hinweisen, dass du etwas zu kompliziert machst. Ziehe in Erwägung, deinen Code umzugestalten und sie zu entfernen.

Es gibt noch eine weitere Stelle, an der du eine break -Anweisung in einer case -Anweisung in einer switch -Anweisung verwenden kannst. Wenn du eine switch Anweisung in einer for Schleife hast und aus der for Schleife ausbrechen willst, füge ein Label in die for Anweisung ein und gib den Namen des Labels in die break Anweisung ein. Wenn du kein Label verwendest, geht Go davon aus, dass du aus der case Schleife ausbrechen willst. Schauen wir uns ein kurzes Beispiel an, in dem du aus der for Schleife ausbrechen willst, sobald sie 7 erreicht. Du kannst den Code in Beispiel 4-20 auf dem Go Playground oder in der Funktion missingLabel in main.go im Verzeichnis sample_code/switch im Repository von Kapitel 4 ausführen.

Beispiel 4-20. Der Fall des fehlenden Etiketts
func main() {
    for i := 0; i < 10; i++ {
        switch i {
        case 0, 2, 4, 6:
            fmt.Println(i, "is even")
        case 3:
            fmt.Println(i, "is divisible by 3 but not 2")
        case 7:
            fmt.Println("exit the loop!")
            break
        default:
            fmt.Println(i, "is boring")
        }
    }
}

Wenn du diesen Code ausführst, erhältst du die folgende Ausgabe:

0 is even
1 is boring
2 is even
3 is divisible by 3 but not 2
4 is even
5 is boring
6 is even
exit the loop!
8 is boring
9 is boring

Das ist nicht das, was beabsichtigt ist. Das Ziel war es, die for -Schleife zu verlassen, wenn sie eine 7 erhält, aber break wurde als Verlassen der case-Schleife interpretiert. Um das Problem zu lösen, musst du eine Beschriftung einführen, genauso wie du es beim Verlassen einer verschachtelten for -Schleife getan hast. Zuerst kennzeichnest du die Anweisung for:

loop:
    for i := 0; i < 10; i++ {

Dann benutzt du das Etikett für deine Pause:

break loop

Du kannst diese Änderungen auf dem Go Playground oder in der Funktion labeledBreak in main.go im Verzeichnis sample_code/switch im Kapitel 4 Repository sehen. Wenn du die Funktion erneut ausführst, erhältst du die erwartete Ausgabe:

0 is even
1 is boring
2 is even
3 is divisible by 3 but not 2
4 is even
5 is boring
6 is even
exit the loop!

Leere Schalter

Du kannst switch Anweisungen auf eine andere, mächtigere Weise verwenden. Genauso wie Go es dir erlaubt, Teile der Deklaration einer for Anweisung wegzulassen, kannst du eine switch Anweisung schreiben, die den Wert, mit dem du vergleichst, nicht angibt. Das nennt man einen Blankoschalter. Mit einer regulären switch kannst du nur einen Wert auf Gleichheit prüfen. Mit einem leeren switch kannst du jeden beliebigen booleschen Vergleich für jede case verwenden. Du kannst den Code in Beispiel 4-21 auf dem Go Playground oder in der Funktion basicBlankSwitch in main.go im Verzeichnis sample_code/blank_switch im Repository von Kapitel 4 ausprobieren.

Beispiel 4-21. Der Rohling switch
words := []string{"hi", "salutations", "hello"}
for _, word := range words {
    switch wordLen := len(word); {
    case wordLen < 5:
        fmt.Println(word, "is a short word!")
    case wordLen > 10:
        fmt.Println(word, "is a long word!")
    default:
        fmt.Println(word, "is exactly the right length.")
    }
}

Wenn du dieses Programm ausführst, erhältst du die folgende Ausgabe:

hi is a short word!
salutations is a long word!
hello is exactly the right length.

Genau wie bei einer regulären switch Anweisung kannst du optional eine kurze Variablendeklaration als Teil deines leeren switch einfügen. Aber anders als bei einer regulären switch kannst du logische Tests für deine Fälle schreiben. Leere Schalter sind ziemlich cool, aber übertreibe es nicht. Wenn du feststellst, dass du eine leere switch geschrieben hast, bei der alle deine Fälle Gleichheitsvergleiche gegen dieselbe Variable sind:

switch {
case a == 2:
    fmt.Println("a is 2")
case a == 3:
    fmt.Println("a is 3")
case a == 4:
    fmt.Println("a is 4")
default:
    fmt.Println("a is ", a)
}

solltest du sie durch eine switch Anweisung ersetzen:

switch a {
case 2:
    fmt.Println("a is 2")
case 3:
    fmt.Println("a is 3")
case 4:
    fmt.Println("a is 4")
default:
    fmt.Println("a is ", a)
}

Die Wahl zwischen if und switch

Was die Funktionalität angeht, gibt es keinen großen Unterschied zwischen einer Reihe von if/else Anweisungen und einer leeren switch Anweisung. Beide ermöglichen eine Reihe von Vergleichen. Wann solltest du also switch verwenden und wann eine Reihe von if oder if/else Anweisungen? Eine switch Anweisung, auch eine leere switch, zeigt an, dass eine Beziehung zwischen den jeweiligen Werten oder Vergleichen besteht. Um den Unterschied in der Klarheit zu verdeutlichen, schreibe das FizzBuzz-Programm aus Beispiel 4-11 mit einer leeren switch neu, wie in Beispiel 4-22gezeigt. Du findest diesen Code auch im Verzeichnis sample_code/simplest_fizzbuzz im Repository von Kapitel 4.

Beispiel 4-22. Umschreiben einer Reihe von if Anweisungen mit einem Leerzeichen switch
for i := 1; i <= 100; i++ {
    switch {
    case i%3 == 0 && i%5 == 0:
        fmt.Println("FizzBuzz")
    case i%3 == 0:
        fmt.Println("Fizz")
    case i%5 == 0:
        fmt.Println("Buzz")
    default:
        fmt.Println(i)
    }
}

Die meisten Leute werden zustimmen, dass dies die lesbarste Version ist. Es gibt keinen Bedarf mehr für continue Anweisungen und das Standardverhalten wird mit dem default case explizit gemacht.

Natürlich hindert dich nichts in Go daran, alle möglichen unzusammenhängenden Vergleiche auf jedem case in einem leeren switch durchzuführen. Das ist jedoch nicht idiomatisch. Wenn du dich in einer Situation befindest, in der du dies tun möchtest, verwende eine Reihe von if/else Anweisungen (oder überlege dir, deinen Code zu überarbeiten).

Tipp

Bevorzuge leere switch Anweisungen gegenüber if/else Ketten, wenn du mehrere zusammenhängende Fälle hast. Die Verwendung von switch macht die Vergleiche sichtbarer und unterstreicht, dass es sich um einen zusammenhängenden Satz von Anliegen handelt.

goto-Ja, goto

Go hat eine vierte Kontrollanweisung, aber die Chancen stehen gut, dass du sie nie benutzen wirst. Seit Edsger Dijkstra 1968 " Go To Statement Considered Harmful" schrieb, ist die Anweisung goto das schwarze Schaf der Coding-Familie. Dafür gibt es gute Gründe. Traditionell war goto gefährlich, weil man damit fast überall im Programm hin springen konnte; man konnte in oder aus einer Schleife springen, Variablendefinitionen überspringen oder in die Mitte einer Reihe von Anweisungen in einer if Anweisung. Das machte es schwierig zu verstehen, was ein goto-benutzendes Programm tat.

Die meisten modernen Sprachen enthalten keine goto. Go hat jedoch eine goto Anweisung. Du solltest trotzdem alles tun, um sie zu vermeiden, aber sie hat ihre Berechtigung und die Einschränkungen, die Go ihr auferlegt, machen sie zu einer besseren Ergänzung für die strukturierte Programmierung.

In Go gibt eine goto Anweisung eine bestimmte Codezeile an, zu der die Ausführung dann springt. Du kannst jedoch nicht überall hin springen. Go verbietet Sprünge, die Variablendeklarationen überspringen, und Sprünge, die in einen inneren oder parallelen Block gehen.

Das Programm in Beispiel 4-23 zeigt zwei illegale goto Anweisungen. Du kannst versuchen, es auf The Go Playground oder im Verzeichnis sample_code/broken_goto im Repository von Kapitel 4 auszuführen.

Beispiel 4-23. Go's goto hat Regeln
func main() {
    a := 10
    goto skip
    b := 20
skip:
    c := 30
    fmt.Println(a, b, c)
    if c > a {
        goto inner
    }
    if a < b {
    inner:
        fmt.Println("a is less than b")
    }
}

Der Versuch, dieses Programm auszuführen, führt zu den folgenden Fehlern:

goto skip jumps over declaration of b at ./main.go:8:4
goto inner jumps into block starting at ./main.go:15:11

Wofür solltest du also goto verwenden? Meistens solltest du das nicht. Mit den Anweisungen break und continue kannst du aus tief verschachtelten Schleifen herausspringen oder eine Iteration überspringen. Das Programm in Beispiel 4-24 hat eine legale goto und demonstriert einen der wenigen gültigen Anwendungsfälle. Du findest diesen Code auch im Verzeichnis sample_code/good_goto im Repository von Kapitel 4.

Beispiel 4-24. Ein Grund für die Verwendung goto
func main() {
    a := rand.Intn(10)
    for a < 100 {
        if a%5 == 0 {
            goto done
        }
        a = a*2 + 1
    }
    fmt.Println("do something when the loop completes normally")
done:
    fmt.Println("do complicated stuff no matter why we left the loop")
    fmt.Println(a)
}

Dieses Beispiel ist erfunden, aber es zeigt, wie goto ein Programm klarer machen kann. In diesem einfachen Fall gibt es eine Logik, die in der Mitte der Funktion nicht ausgeführt werden soll, aber am Ende der Funktion. Es gibt Möglichkeiten, dies ohne goto zu tun. Du könntest ein boolesches Flag einrichten oder den komplizierten Code nach der for Schleife duplizieren, anstatt eine goto zu haben, aber beide Ansätze haben Nachteile. Wenn du deinen Code mit booleschen Flags versiehst, um den logischen Ablauf zu steuern, ist das wohl die gleiche Funktionalität wie die goto Anweisung, nur etwas ausführlicher. Das Duplizieren von kompliziertem Code ist problematisch, weil es die Wartung deines Codes erschwert. Diese Situationen sind zwar selten, aber wenn du keine Möglichkeit findest, deine Logik umzustrukturieren, verbessert die Verwendung von goto deinen Code tatsächlich.

Wenn du ein Beispiel aus der Praxis sehen willst, kannst du dir die Methode floatBits in der Datei atof.go im Paket strconv in der Standardbibliothek ansehen. Sie ist zu lang, um sie in ihrer Gesamtheit aufzunehmen, aber die Methode endet mit diesem Code:

overflow:
    // ±Inf
    mant = 0
    exp = 1<<flt.expbits - 1 + flt.bias
    overflow = true

out:
    // Assemble bits.
    bits := mant & (uint64(1)<<flt.mantbits - 1)
    bits |= uint64((exp-flt.bias)&(1<<flt.expbits-1)) << flt.mantbits
    if d.neg {
        bits |= 1 << flt.mantbits << flt.expbits
    }
    return bits, overflow

Vor diesen Zeilen gibt es mehrere Bedingungsprüfungen. Einige erfordern, dass der Code nach dem Label overflow ausgeführt wird, während andere Bedingungen erfordern, dass dieser Code übersprungen und direkt zu out übergegangen wird. Je nach Bedingung gibt es goto Anweisungen, die zu overflow oder out springen. Du könntest wahrscheinlich einen Weg finden, die goto Anweisungen zu vermeiden, aber sie machen den Code schwerer zu verstehen.

Tipp

Du solltest versuchen, goto möglichst zu vermeiden. Aber in den seltenen Fällen, in denen es deinen Code lesbarer macht, ist es eine Option.

Übungen

Jetzt ist es an der Zeit, alles, was du über Kontrollstrukturen und Blöcke gelernt hast, in Go anzuwenden. Die Antworten auf diese Übungen findest du im Repository von Kapitel 4.

  1. Schreibe eine for Schleife, die 100 Zufallszahlen zwischen 0 und 100 in ein int Slice einträgt.

  2. Ziehe eine Schleife über den Slice, den du in Übung 1 erstellt hast. Wende für jeden Wert in der Scheibe die folgenden Regeln an:

    1. Wenn der Wert durch 2 teilbar ist, drucke "Zwei!"

    2. Wenn der Wert durch 3 teilbar ist, drucke "Drei!"

    3. Wenn der Wert durch 2 und 3 teilbar ist, drucke "Sechs!". Drucke sonst nichts aus.

    4. Ansonsten drucke "Vergiss es".

  3. Starte ein neues Programm. Deklariere in main eine int Variable namens total. Schreibe eine for Schleife, die eine Variable namens i benutzt, um von 0 (einschließlich) bis 10 (ausschließlich) zu iterieren. Der Körper der for Schleife sollte wie folgt aussehen:

    total := total + i
    fmt.Println(total)

    Gib nach der Schleife for den Wert von total aus. Was wird ausgedruckt? Was ist der wahrscheinliche Fehler in diesem Code?

Einpacken

In diesem Kapitel wurden viele wichtige Themen behandelt, um Go idiomatisch zu schreiben. Du hast etwas über Blöcke, Shadowing und Kontrollstrukturen gelernt und wie du sie richtig einsetzt. Jetzt bist du in der Lage, einfache Go-Programme zu schreiben, die in die Funktion main passen. Jetzt ist es an der Zeit, zu größeren Programmen überzugehen und Funktionen zu verwenden, um deinen Code zu organisieren.

Get Go lernen, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.