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?
Steve van de Grens schrieb:> Warum ist das nicht der Fall?
Aus dem selben Grund, warum
``` go
package main
import "fmt"
func main() {
productData := 123
oldData := &productData
productData = 0
fmt.Printf("productData=%v, oldData=%v \n", productData, oldData)
}
```
auch für beide Variablen 0 zurück gibt.
`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:
```go
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:
```go
import "slices"
...
oldData := slices.Clone(productData)
```
Franko S. schrieb:> Weil der GC vorher aufräumt?
Das hat nichts mit dem GC zu tun. Der räumt hier natürlich das Array
bzw. den Slice auf, aber nur weil gar keine referenz mehr darauf
vorhanden ist.
Norbert schrieb:> Github != µC.net ;-)
Komischerweise hat die Vorschau Markdown korrekt gerendert. Ich bin
einfach davon ausgegangen, dass der Beitrag genauso wie die Vorschau
aussieht.
David V. schrieb:> 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.
David V. schrieb:> Das hat nichts mit dem GC zu tun. Der räumt hier natürlich das Array> bzw. den Slice auf, aber nur weil gar keine referenz mehr darauf> vorhanden ist.
Eben, deshalb ist auch nix mehr da.
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.
Hans schrieb:> 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.
Sheeva P. schrieb:> [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.
Michael
Ich denke, der wesentliche Knackpunkt is, dass ich "nil" aus Gewohnheit
mit einem Zeiger assoziiere, was hier aber nicht der Fall ist.
David V. schrieb:> Aus dem selben Grund, warum
1
productData:=123
2
oldData:=&productData
3
productData=0
> auch für beide Variablen 0 zurück gibt.
Oben wird productData auf 0 gesetzt, sonnenklar.
Und dort
1
productData:=[]string{"A","B","C"}
2
oldData:=&productData
3
productData=nil
wird productData leer gemacht (auf den Wert des leeren nil slice
gesetzt). Ich werde mich daran gewöhnen.
Steve van de Grens schrieb:> 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.
LG, Sebastian
Steve van de Grens schrieb:> 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.
Sebastian W. schrieb:> Das &-Zeichen in der Ausgabe ist dabei beachtenswert.
Stimmt, das habe ich auch übersehen. Meine Blödsinnsantwort mit dem GC
bitte ignorieren. ;)