Skip to content

Referenz

Harald Weidner edited this page Feb 20, 2020 · 1 revision

Referenzsemantik in Go

Call by value

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.

Call by reference durch Pointer

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
    ...
}

String

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.

Slice

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.

Map

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.

Channel

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.

Flache und tiefe Kopien

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.