Heim >Backend-Entwicklung >Golang >Arrays vs. Slices in Go: Die Funktionsweise „unter der Haube' visuell verstehen
Haben Sie schon einmal versucht, für eine Reise zu packen, ohne zu wissen, wie lange Sie dort bleiben werden? Genau das passiert, wenn wir Daten in Go speichern. Manchmal, etwa beim Packen für einen Wochenendausflug, wissen wir genau, wie viele Dinge wir verstauen müssen; In anderen Fällen, beispielsweise wenn wir für eine Reise packen und sagen: „Ich komme zurück, wenn ich bereit bin“, tun wir das nicht.
Lassen Sie uns tief in die Welt der Go-Arrays eintauchen und die Interna anhand einfacher Illustrationen aufschlüsseln. Wir werden Folgendes untersuchen:
Am Ende dieser Lektüre werden Sie mithilfe von Beispielen aus der Praxis und Speicherdiagrammen verstehen können, wann Arrays und wann Slices zu verwenden sind
Stellen Sie sich ein Array als einen einzelnen Speicherblock vor, in dem jedes Element nebeneinander sitzt, wie eine Reihe perfekt angeordneter Kästchen.
Wenn Sie var-Zahlen [5]int deklarieren, reserviert Go genau genug zusammenhängenden Speicher, um 5 ganze Zahlen aufzunehmen, nicht mehr und nicht weniger.
Da sie über zusammenhängenden festen Speicher verfügen, kann die Größe während der Laufzeit nicht geändert werden.
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
Die Größe ist Teil des Array-Typs. Das bedeutet, dass [5]int und [6]int völlig unterschiedliche Typen sind, genau wie int und string unterschiedlich sind.
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
Wenn Sie Arrays in Go zuweisen oder übergeben, werden standardmäßig Kopien erstellt. Dies stellt die Datenisolation sicher und verhindert unerwartete Mutationen.
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
Okay, Sie können also nicht vardynamic [size]int verwenden, um die dynamische Größe festzulegen. Hier kommt Slice ins Spiel.
Der Zauber liegt darin, wie es diese Flexibilität beibehält und gleichzeitig den Betrieb schnell hält.
Jedes Slice in Go besteht aus drei kritischen Komponenten:
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
Was ist unsicher.Pointer??
Der unsafe.Pointer ist Gos Methode zum Umgang mit Rohspeicheradressen ohne Typsicherheitseinschränkungen. Es ist „unsicher“, da es das Typsystem von Go umgeht und eine direkte Speichermanipulation ermöglicht.
Betrachten Sie es als das Äquivalent von Go zum Void-Zeiger von C.
Was ist das für ein Array?
Wenn Sie ein Slice erstellen, weist Go einen zusammenhängenden Speicherblock im Heap zu (im Gegensatz zu Arrays), der als Backing-Array bezeichnet wird. Jetzt zeigt das Array in der Slice-Struktur auf den Anfang dieses Speicherblocks.
Das Array-Feld verwendet unsafe.Pointer, weil:
Lassen Sie uns versuchen, eine Intuition für den eigentlichen Algorithmus unter der Haube zu entwickeln.
Wenn wir unserer Intuition folgen, können wir zwei Dinge tun:
Wir könnten so viel Platz reservieren und ihn nach Bedarf nutzen
Vorteile: Bewältigt wachsende Bedürfnisse bis zu einem bestimmten Punkt
Nachteile: Speicherverschwendung, praktisch könnte das Limit erreicht werden
Wir könnten zunächst eine zufällige Größe festlegen und beim Anhängen der Elemente können wir den Speicher bei jedem Anhängen neu zuweisen
Vorteile: Behandelt den vorherigen Fall, kann je nach Bedarf erweitert werden
Nachteile: Neuzuweisung ist teuer und bei jedem Anhang wird es schlimmer
Wir können die Neuzuweisung nicht vermeiden, denn wenn die Kapazität erreicht ist, muss man wachsen. Wir können die Neuzuweisung minimieren, sodass die Kosten für nachfolgende Einfügungen/Anhänge konstant sind (O(1)). Dies wird als fortgeführte Anschaffungskosten bezeichnet.
Wie können wir das angehen?
bis zur Go-Version v1.17 wurde folgende Formel verwendet:
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
ab Go-Version v1.18:
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
Da das Verdoppeln einer großen Scheibe Speicherverschwendung ist, nimmt der Wachstumsfaktor mit zunehmender Scheibengröße ab.
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
Lassen Sie uns einige Elemente zu unserem Slice hinzufügen
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
Da wir Kapazitäten haben (5) > Länge (3), Los:
Verwendet vorhandenes Backing-Array
Platziert 10 auf Index 3
Erhöht die Länge um 1
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
Lass uns ans Limit gehen
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
Ups! Jetzt, wo wir unsere Kapazitäten erreicht haben, müssen wir wachsen. Folgendes passiert:
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
Was passiert, wenn es eine große Scheibe ist?
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
Da die Kapazität 256 beträgt, verwendet Go die Wachstumsformel nach 1,18:
Neue Kapazität = oldCap oldCap/4
256 256/4 = 256 64 = 320
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
So sehen die Slice-Header aus:
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
Versehentliche Aktualisierungen
Da Slice Referenzsemantik verwendet, werden keine Kopien erstellt, die bei Unachtsamkeit zu einer versehentlichen Mutation zum Original-Slice führen könnten.
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
Teurer Anhängevorgang
numbers := make([]int, 3, 5) // length=3 capacity // Memory Layout after creation: Slice Header: { array: 0xc0000b2000 // Example memory address len: 3 cap: 5 } Backing Array at 0xc0000b2000: [0|0|0|unused|unused]
Kopieren vs. Anhängen
numbers = append(numbers, 10)
Lassen Sie uns das mit einem klaren Auswahlleitfaden abschließen:
? Wählen Sie Arrays, wenn:
? Wählen Sie Slices, wenn:
? Schauen Sie sich das notion-to-md-Projekt an! Es handelt sich um ein Tool, das Notion-Seiten in Markdown konvertiert und sich perfekt für Inhaltsersteller und Entwickler eignet. Treten Sie unserer Discord-Community bei.
Das obige ist der detaillierte Inhalt vonArrays vs. Slices in Go: Die Funktionsweise „unter der Haube' visuell verstehen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!