Nil slice in golang

Gast #7609605
Lesenswert?

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:

1
package main
2
import "fmt"
3
func main() {
4

5
  productData := []string{"A", "B", "C"}
6

7
  oldData := &productData
8
  productData = nil
9

10
  fmt.Printf("productData=%v, oldData=%v \n", productData, oldData)
11
}

Ausgabe:

1
productData=[], oldData=&[]

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?

#7609919
Lesenswert?

Steve van de Grens schrieb:

Warum ist das nicht der Fall?

Aus dem selben Grund, warum

1
package main
2

3
import "fmt"
4

5
func main() {
6

7
  productData := 123
8

9
  oldData := &productData
10
  productData = 0
11

12
  fmt.Printf("productData=%v, oldData=%v \n", productData, oldData)
13

14
}

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:

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:

1
import "slices"
2
...
3
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.

Gast #7609990
Lesenswert?

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.

#7610036
Lesenswert?

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.

#7610107
Lesenswert?

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.

#7610117
Lesenswert?

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

Gast #7610118
Lesenswert?

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.

#7610148
Lesenswert?

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

#7610242
Lesenswert?

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.
19
fmt.Println(oldDataCopy1)       // Ergibt [A B C]
20
fmt.Println(oldDataCopy2)       // Ergibt [A B C]
21
fmt.Println(oldDataShallowCopy) // Ergibt [A B C]
22

23
oldDataCopy1[1] = "F"
24

25
fmt.Println(productData)        // Ergibt []
26
fmt.Println(oldDataRef)         // Ergibt &[]
27
fmt.Println(oldDataCopy1)       // Ergibt [A F C]
28
fmt.Println(oldDataCopy2)       // Ergibt [A F C]
29
fmt.Println(oldDataShallowCopy) // Ergibt [A B C]

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren