In Go kann die Schleifenstruktur for einen Initialisierungsblock (init), eine Bedingungsprüfung und einen nachgestellten Ausdruck umfassen. Historisch wurde dieser Mechanismus für die Bequemlichkeit beim Schreiben von Code und aus der Gewohnheit an C-ähnliche Sprachen entwickelt. Allerdings hat in Go der Gültigkeitsbereich der Schleifenvariable (i) Besonderheiten, die das Verhalten innerhalb von verschachtelten Funktionen, Closures und Goroutinen stark beeinflussen.
Problem — Bei der Ausführung von Goroutinen oder Closures in jeder Schleifeniteration tritt häufig unerwartetes Verhalten auf: Die Variable i wird nicht kopiert, sondern "durch Referenz erfasst", das bedeutet, das Closure greift auf die gemeinsame Schleifenvariable zu, die nach Ende der Schleife den letzten Wert annimmt. Dies führt dazu, dass alle Goroutinen/Closures dasselbe Ergebnis liefern, obwohl die Logik etwas anderes hätte vermuten lassen.
Lösung — Wenn notwendig, den Wert der Variablen jeder Iteration zu übergeben, verwenden Sie eine explizite Kopie der Variable (durch eine zusätzliche Variable) oder übergeben Sie sie als Argument an das Closure.
Beispielcode:
for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // Richtig! Kopierter Wert } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Fehler: Alle Goroutinen drucken 3 }
Wesentliche Merkmale:
Ändert sich der Gültigkeitsbereich der for-Variable bei Verwendung von break oder continue?
Nein. Der Gültigkeitsbereich der im for deklarierten Variable ist immer auf den Block dieser Schleife beschränkt. Break oder continue unterbrechen nur die jeweilige Iteration, übergeben jedoch nicht die Variable nach außen.
Kann eine Variable, die im init-Teil von for deklariert wurde, innerhalb einer Methode außerhalb der Schleife erfasst werden?
Nein. Die Variable ist nur innerhalb des for selbst und aller darin enthaltenen Blöcke sichtbar, jedoch nicht nach dem Ende der Schleife außerhalb.
Was passiert, wenn eine Variable in einem defer-Ausdruck innerhalb von for erfasst wird?
Die gleiche Situation: Die defer-Funktion "sieht" nicht den Wert zum Zeitpunkt der Erstellung, sondern den aktuellen Wert der Variablen zum Zeitpunkt der Ausführung des defer (in der Regel — den letzten Wert der Schleife).
for i := 0; i < 3; i++ { defer fmt.Println(i) // Alle defer drucken 3 }
In einem Go-Webserver startete der Entwickler mehrere Goroutinen zur Verarbeitung verschiedener Ports, indem er den Portindex als Schleifenvariable verwendete und ihn direkt im Lambda-Ausdruck erfasste. Alle Goroutinen gingen auf denselben Port — den letzten im Array.
Vorteile:
Nachteile:
Im Team wurde eine Regel eingeführt — immer den Wert der Schleifenvariable in eine neue Variable zu kopieren, die dann vom Closure/Goroutine erfasst wird.
Vorteile:
Nachteile: