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
"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.
Ü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.
-
Schreibe eine
for
Schleife, die 100 Zufallszahlen zwischen 0 und 100 in einint
Slice einträgt. -
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:
-
Wenn der Wert durch 2 teilbar ist, drucke "Zwei!"
-
Wenn der Wert durch 3 teilbar ist, drucke "Drei!"
-
Wenn der Wert durch 2 und 3 teilbar ist, drucke "Sechs!". Drucke sonst nichts aus.
-
Ansonsten drucke "Vergiss es".
-
-
Starte ein neues Programm. Deklariere in
main
eineint
Variable namenstotal
. Schreibe einefor
Schleife, die eine Variable namensi
benutzt, um von 0 (einschließlich) bis 10 (ausschließlich) zu iterieren. Der Körper derfor
Schleife sollte wie folgt aussehen:total
:=
total
+
i
fmt
.
Println
(
total
)
Gib nach der Schleife
for
den Wert vontotal
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.