-
Notifications
You must be signed in to change notification settings - Fork 0
Referenz
Wie die meisten imperativen Programmiersprachen hat Go eine weitgehend strikte Call by value Semantik. Beim Aufruf einer Funktion mit Parametern erhält diese eine Kopie der übergebenen Daten. Ebenso wird bei einer Zuweisung eine Kopie des Objektes erzeugt.
c = foo(a, b)
y = x
Die Funktion foo()
arbeitet mit einer Kopie der Variablen a und b.
Auch wenn innerhalb der Funktion die Parameter verändert wurden, bleiben
sie für das Hauptprogramm gleich. Ebenso existiert nach der Zuweisung
eine Kopie der Variablen x
, die separat Speicher belegt und bezüglich
des Wertes unabhängig vom Original ist.
Das eben Gesagte gilt vorbehaltlos für alle skalaren Datentypen (z.B.
int
, float64
, bool
), für Arrays (z.B. [20]uint32
) und
Verbundtypen (struct
). Prinzipiell gilt es für alle Typen, allerdings
ist der Effekt bei einigen Typen aufgrund ihrer Referenzsemantik ein
anderer. Diese Ausnahmen sind im Folgenden aufgeführt.
In manchen Fällen ist eine Call by reference Semantik sinnvoll. Gründe dafür können sein:
- Der übergebene Datentyp ist sehr groß (z.B. ein Array mit vielen Elementen oder ein großer Struct), so dass bei Call by value zu große Daten kopiert werden müssten
- Die aufgerufene Funktion/Methode soll die Daten ändern können
In diesem Fall kann ein Pointer auf die Daten als Übergabeparameter sinnvoll sein. Beispiel:
func foo(a *int) {
*a *= 2
}
func main() {
...
x := 1
foo(&x) // x ist nun 2
...
}
In Go ist ein String technisch gesehen eine Datenstruktur, in dem die Länge des Strings und eine Referenz auf ein (anonymes) Array von Bytes gespeichert sind. Beim Funktionsaufruf wird daher nicht der String selber, sondern nur diese Datenstruktur kopiert. Original und Kopie verweisen auf das selbe anonyme Array und damit auf die selben Daten.
Da ein String in Go nicht änderbar ist, spielt das jedoch für den Programmierer nur eine untergeordnete Rolle. Es ist nicht möglich, in einer Unterfunktion die Zeichen des Strings zu ändern, so dass die aufrufende Funktion die Änderungen sieht. Wenn der Variablen in der Unterfunktion ein neuer String zugeordnet wird, ist dies technisch eine neue Datenstruktur mit Verweis auf ein neues anonymes Array. Semantisch gibt es somit keinen Unterschied zum Call by value. Da beim Funktionsaufruf nur die Datenstruktur (Länge und Referenz auf Array) kopiert wird, können auch sehr lange Strings effizient an Funktionen übergeben werden.
Ein Slice in Go ist eine Datenstruktur, die die Länge des Slice, die Kapazität und eine Referenz auf ein anonymes Array passender Größe enthält. Beim Funktionsaufruf wird nur die Datenstruktur kopiert, nicht das anonyme Array. Im Gegensatz zum String sind bei einem Slice die Daten änderbar. Daher gelten folgende Regeln für die Referensemantik:
- Überschreibt eine aufgerufene Funktion die Daten in dem anonymen Array, dann sind diese Änderungen in der aufrufenden Funktion sichtbar.
- Werden dagegen die Metadaten in der Datenstruktur geändert (z.B. die Länge des Slice), dann geschieht das in der Kopie der Datenstruktur, somit sind die Änderungen für die aufrufende Funktion nicht sichtbar.
Vorsicht ist geboten bei Verwendung der Funktion append()
. Wenn die
Kapazität des Slice für die eingefügten Daten ausreichend ist, dann
ändert sich lediglich der Inhalt des anonymen Arrays; wenn dagegen eine
Kapazitätserweiterung nötig ist, wird ein neuer Slice erzeugt, der auf
ein neues, größeres anonymes Array referenziert. Somit ist nicht
unmittelbar ersichtlich, ob die Daten, die mit append
an einen Slice
angehängt werden, für die aufrufende Funktion sichtbar sind.
Als Merkregel gilt daher:
- Bei Funktionen, die lediglich die Inhalte eines Slice manipulieren, genügt es, den Slice als Übergabeparameter zu definieren.
- Wenn die Funktion potenziell Daten an den Slice anhängt und dies für die aufrufende Funktion sichtbar sein soll, dann muss ein Pointer auf den Slice übergeben werden.
- Wenn es nicht gewollt ist, dass die aufrufende Funktion die
Änderungen an einem Slice sieht, dann muss innerhalb der
aufgerufenen Funktion zunächst mit
copy()
eine Kopie des Slice erstellt werden.
Eine Map ist ein assoziatives Array. Ähnlich wie ein Slice ist eine Map ein Referenztyp. D.h. beim Aufruf einer Funktion mit einer Map als Argument oder bei der Zuweisung einer Map an eine Variable passenden Typs wird lediglich eine Referenz kopiert. Die neue Variable bzw. die aufgerufene Funktion sehen die gleichen Inhalte der Map. Änderungen an der Map durch eine aufgerufene Funktion sind in der aufrufenden Funktion sichtbar.
Eine eingebaute Funktion zum Kopieren einer Map gibt es in Go nicht.
Wenn eine Kopie erstellt werden soll, muss dies explizit in einer
for ... range
Schleife ausprogrammiert werden.
Ein Channel ist ein Referenztyp. Nach einer Zuweisung einer
Channelvariable an eine andere verweisen beide auf den selben Channel;
es ist möglich, in den einen zu schreiben und von dem anderen zu lesen.
Gleiches gilt beim Aufruf einer Funktion oder Goroutine. Es ist nicht
möglich, einen Channel zu kopieren. Daten, die in einem gepufferten
Channel “gespeichert” sind, können nur durch Auslesen gelesen werden und
sind danach nicht mehr im Channel vorhanden. Wenn ein neuer,
unabhängiger Channel benötigt wird, muss er mit make()
erzeugt werden.
Ein Verbundtyp (struct) wird bei einer Zuweisung oder Parameterübergabe an eine Unterfunktion “flach” kopiert. Das heisst, die einzelnen Elemente des neuen Struct erhalten ihre Inhalte durch Zuweisung von den entsprechenden Elementen des Original-Struct. Bei einem Referenztyp (z.B. einem Slice) bedeutet das, dass der alte und neue Struct anschließend auf die selben Daten verweisen; eine Änderung an den Inhalten des Slice im neuen Struct ändert auch die Inhalte des alten und umgekehrt.
Für eine “tiefe” Kopie, bei der die Daten abhängig vom Datentyp rekursiv umkopiert werden, gibt es in Go kein direktes Sprachmittel. Eine gängige Vorgehensweise ist, die Datenstruktur in ein gängiges Format zu serialisieren (z.B. JSON oder GOB) und wieder zu deserialisieren.