Forum: PC-Programmierung Nil slice in golang


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Steve van de Grens (roehrmond)


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?

: Bearbeitet durch User
von Franko S. (frank_s866)


Lesenswert?

Steve van de Grens schrieb:
> Warum ist das nicht der Fall?
Weil der GC vorher aufräumt? Ist halt kein C sondern Go.

von Norbert (der_norbert)


Lesenswert?

Erinnert so ein bisschen an deepcopy vs. shallowcopy in Python.

von David V. (dadido3)


Lesenswert?

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.

von Norbert (der_norbert)


Lesenswert?

David V. schrieb:
> ```

Github != µC.net   ;-)

von David V. (dadido3)


Lesenswert?

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.

von Norbert (der_norbert)


Lesenswert?

Das ist ja seltsam. Ich kenne hier nur die code Tags. Aber vielleicht 
gibt's ja bald Änderungen…

von Steve van de Grens (roehrmond)


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.

: Bearbeitet durch User
von Franko S. (frank_s866)


Lesenswert?

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.

von Hans (ths23)


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.

von Sheeva P. (sheevaplug)


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.

von Michael D. (nospam2000)


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

von Steve van de Grens (roehrmond)


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.

: Bearbeitet durch User
von Sebastian W. (wangnick)


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

von David V. (dadido3)


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]

von Sebastian W. (wangnick)


Lesenswert?

Steve van de Grens schrieb:
1
> productData=[], oldData=&[]

Das &-Zeichen in der Ausgabe ist dabei beachtenswert.

LG, Sebastian

von Franko S. (frank_s866)


Lesenswert?

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. ;)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.