Wo wir gerade in einem anderen Thread über Go diskutieren, dachte ich, ich kann die Frage hier stellen. Heute hat ein Kollege einen Bug in meinem Code gefunden, den man so reproduzieren kann:
Sobald die Zeile "productData = nil" ausgeführt wurde, zeigt auch oldData auf einen leeren Slice. Das habe ich nicht erwartet.
Nach meinem Verständnis biege ich mit dieser Zeile productData auf nichts um. Die alten Daten müssten also weiterhin noch an der alten Stelle im Speicher stehen. Warum ist das nicht der Fall?
oldData referenziert den Wert, welcher in productData gespeichert ist. Wenn du productData änderst (oder auf nil setzt), dann wird das natürlich auch über die referenz oldData so wiedergegeben.
Wenn es deine Absicht ist den Slice zu kopieren, dann solltest du auch einfach nur den Wert kopieren:
1
oldData:=productData
Das ist aber nur eine Kopie des Slices, und nicht des zugrunde liegenden Arrays auf das der Slice verweist. Wenn es deine Absicht ist eine shallow copy von dem Slice (und dessen Daten) zu erzeugen, dann geht das am besten mit:
Das ist aber nur eine Kopie des Slices, und nicht des zugrunde liegenden
Arrays auf das der Slice verweist.
Ich habe die fehlerhafte Stelle schon durch eine shallow-copy ersetzt.
Ich denke wohl noch zu sehr an C, wo nil/null ein Zeiger ist, nicht ein Wert. Für einen neuen Entwickler ist das Verhalten von Go vermutlich total logisch, für mich nicht so sehr. Da muss ich durch, denn das ist nun mein Arbeitsmittel, auf das ich mich eingelassen habe.
Du weist Deinem Olddata einen Zeiger auf Irgendetwas zu, d.h. Olddata und der Zeiger auf Irgendetwas sind identisch, zeigen am Ende also auf den gleichen Speicherplatz. Wenn Du Irgendetwas löschst, indem Du es auf nil zeigen läßt, dann zeigt halt auch Olddata auf nil, weil es auf die gleiche Adresse wie Irgendetwas zeigt.
Du mußt quasi eine Kopie von Irgendetwas anlegen und Dein Olddata darauf zeigen lassen.
Du weist Deinem Olddata einen Zeiger auf Irgendetwas zu, d.h. Olddata
und der Zeiger auf Irgendetwas sind identisch, zeigen am Ende also auf
den gleichen Speicherplatz. Wenn Du Irgendetwas löschst, indem Du es auf
nil zeigen läßt, dann zeigt halt auch Olddata auf nil, weil es auf die
gleiche Adresse wie Irgendetwas zeigt.
Es sind keine aber Zeiger, sondern Referenzen. Beachte die Unterschiede!
Ein Zeiger ist ein eigenes Objekt mit einer eigenen Speicheradresse und einem Inhalt, das die Speicheradresse einer anderen Variablen ist. Deswegen muß man Zeiger dereferenzieren oder vielleicht besser: "entzeigerisieren". Dagegen ist eine Referenz lediglich ein Alias für eine Variable und hat also keine eigene Speicheradresse, und darum müssen Referenzen nicht dereferenziert werden.
Besonders wichtig wird das beim Aufruf von Funktionen und Methoden. Wir müssen dort -- ok, je nach Sprache -- drei Möglichkeiten unterscheiden, wie Dinge an die Funktion oder Methode übergeben werden können: call-by-value, call-by-pointer und call-by-reference (CBV, CBP, CBR).
Beim CBV wird das Programm (bei Linux per brk(2), sbrk(2) oder mmap(2)) neuen Arbeitsspeicher vom System anfordern und braucht also (mindestens) zwei teure Kontextwechsel für die Systembefehle und danach eine, je nach Größe der Daten oft teure Zeit, um Speicherinhalte vom einen in einen anderen Speicherbereich zu kopieren. Nicht so schön, think Mem-I/O.
Beim CBP werden ebenfalls zwei teure Kontextwechsel nötig, denn auch dort muß Speicher für den Zeiger allokiert werden. Im Prinzip gilt dabei dasselbe wie für CBV, nur daß der Value hier viel kleiner ist - ist ja nur der Zeiger.
Beim CBR geht es nur um eine Referenz, also um einen Alias. Wenn man ein wenig mutig ist, kann man das so sehen wie eine Variable im übergeordneten Scope der Funktion: der Compiler weiß, daß die Daten unter diesem Namen in der Funktion zur Verfügung stellen sollen, und kümmert sich selbständig um den Rest. Es muß dabei kein Syscall, kein Kontextwechsel und keine Speicheroperation ausgeführt werden, sondern es geht nur darum, die im Speicher vorhandenen Daten in einer Funktion unter einem definierten Namen verfügbar zu machen.
Diese Zusammenhänge gehen im Eifer des Gefechts manchmal ein wenig unter, sind für die Performance des Programms aber extrem wichtig. Kontextwechsel sind bei allen mir bekannten Betriebssystemen sehr teuer (langsam) -- und bei größeren Datenmengen sind das natürlich auch Kopieroperationen... und von NUMA oder den internen Optimierungen verschiedener Hard- und Softwaresysteme haben wir hier noch nicht einmal ansatzweise geredet.
[CBR]
kein Kontextwechsel und keine Speicheroperation ausgeführt werden, sondern es
geht nur darum, die im Speicher vorhandenen Daten in einer Funktion
unter einem definierten Namen verfügbar zu machen.
das geht unabhängig von der Programmiersprache nur dann, wenn der aufgerufene code jede Aufrufstelle kennt und quasi inlined wird. Ansonsten muss immer zumindest ein pointer in einem bestimmten Register oder über den Stack übergeben werden.
Ich denke, der wesentliche Knackpunkt is, dass ich "nil" aus Gewohnheit
mit einem Zeiger assoziiere, was hier aber nicht der Fall ist.
Das ist eher nicht der Knackpunkt, denn was auch immer "nil" sein mag spielt im obigen Beispiel ja gar keine Rolle. Der Punkt ist wohl, dass oldData eine Referenz auf productData ist, und nicht (wie von dir vielleicht angenommen) eine (zweite) Referenz auf den Inhalt von productData.
Ich denke, der wesentliche Knackpunkt is, dass ich "nil" aus Gewohnheit
mit einem Zeiger assoziiere, was hier aber nicht der Fall ist.
Das stimmt schon, ein Slice ist intern ein Zeiger bzw. eine Referenz auf ein Array. Wenn du productData = nil setzt, dann zeigt der Slice einfach auf nichts mehr, was einem leeren Slice entspricht. Das hat aber nichts mit dem Problem zu tun. Warum dein erstes Beispiel sich aber so verhält wie es sich verhält liegt daran, dass oldDataRef auf den Speicher von productData zeigt.
Es spielt also keine Rolle ob productData ein Array, Slice, Integer, String oder sonstwas ist. Wenn du productData veränderst, dann spiegelt sich das in den Referenzen darauf wider.
Hier noch ein paar Beispiele die zeigen was intern passiert:
1
productData := []string{"A", "B", "C"}
2
oldDataRef := &productData // oldDataRef zeigt auf den Wert von productData. Also eine Referenz auf productData.
3
oldDataCopy1 := productData // oldDataCopy1 ist eine kopie von productData. Der kopierte Slice zeigt aber auf die selben Daten wie productData.
4
oldDataCopy2 := productData
5
oldDataShallowCopy := slices.Clone(productData) // Dies ergibt einen Slice, welcher auf ein neues Array verweist. Alle Daten wurden hier kopiert.
6
7
// Speicheradressen.
8
fmt.Printf("%p\n", productData) // Ergibt z.B. 0xc0000160f0
9
fmt.Printf("%p\n", oldDataRef) // Ergibt z.B. 0xc000010018 // Das ist die Adresse welche auf den Inhalt von productData zeigt. Also das ist ein Pointer auf den Slice.
10
fmt.Printf("%p\n", *oldDataRef) // Ergibt z.B. 0xc0000160f0 // Durch Dereferenzierung kommt man an den Pointer auf den der Slice selbst zeigt.
11
fmt.Printf("%p\n", oldDataCopy1) // Ergibt z.B. 0xc0000160f0
12
fmt.Printf("%p\n", oldDataCopy2) // Ergibt z.B. 0xc0000160f0
13
fmt.Printf("%p\n", oldDataShallowCopy) // Ergibt z.B. 0xc000016120
14
15
productData = nil
16
17
fmt.Println(productData) // Ergibt []
18
fmt.Println(oldDataRef) // Ergibt &[] // Also ein Zeiger auf einen leeren Slice.