Go Slices verstehen: Dynamische Arrays in Aktion
James Reed
Infrastructure Engineer · Leapcell

Go's Slicetype ist ein leistungsstarker und flexibler Konstrukt, der das Herzstück vieler Go-Programme bildet. Obwohl sie oft konzeptionell mit dynamischen Arrays verglichen werden, ist es wichtig zu verstehen, dass Slices keine Arrays selbst sind, sondern vielmehr Views oder Referenzen auf zugrunde liegende Arrays. Diese Unterscheidung ist entscheidend, um ihr Verhalten, ihre Leistungseigenschaften und ihre effektive Nutzung zu verstehen.
Was ist ein Slice? Ein Blick auf ein Array
Im Kern ist ein Go-Slice eine Datenstruktur, die aus drei Komponenten besteht:
- Ein Zeiger (Pointer): Dieser zeigt auf das erste Element des zugrunde liegenden Arrays, auf das der Slice verweist. Es ist nicht unbedingt der Anfang des zugrunde liegenden Arrays selbst, sondern der Startpunkt der Sicht des Slices.
- Länge (len): Dies ist die Anzahl der Elemente, auf die derzeit über den Slice zugegriffen werden kann. Sie repräsentiert die Länge der Ansicht.
- Kapazität (cap): Dies ist die Anzahl der Elemente im zugrunde liegenden Array, beginnend mit dem Zeiger des Slices, die dem Slice zur Verfügung stehen, ohne dass eine Neuallokation erforderlich ist. Sie repräsentiert die maximale Ausdehnung der potenziellen Ansicht.
Stellen Sie sich visuell ein zugrunde liegendes Array im Speicher vor. Ein Slice definiert lediglich ein Fenster über einen zusammenhängenden Teil dieses Arrays.
// Ein zugrunde liegendes Array var underlyingArray [10]int // Ein Slice 's', der einen Teil von underlyingArray betrachtet // s zeigt auf Index 2 von underlyingArray // s hat eine Länge von 3 (Elemente an den Indizes 2, 3, 4) // s hat eine Kapazität von 8 (Elemente von Index 2 bis 9 sind verfügbar) s := underlyingArray[2:5]
Dieses Design bietet immense Flexibilität. Mehrere Slices können auf dasselbe zugrunde liegende Array verweisen, sich potenziell überlappen oder verschiedene Segmente betrachten. Dieses Verhalten ist entscheidend, wenn man Operationen wie copy
und wie Änderungen an einem Slice durch einen anderen sichtbar sein könnten, versteht.
Slices erstellen
Es gibt mehrere Möglichkeiten, Slices in Go zu erstellen:
1. Slicing eines vorhandenen Arrays oder Slice
Dies ist die gebräuchlichste Methode, einen Slice zu erstellen, indem man ein vorhandenes Array oder einen Slice nutzt. Die Syntax a[low:high]
erstellt einen Slice, der Elemente von low
bis (ausschließlich) high
enthält.
arr := [5]int{10, 20, 30, 40, 50} // Slice von Index 1 (einschließlich) bis Index 4 (ausschließlich) s1 := arr[1:4] // s1 == {20, 30, 40} fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1)) // s1: [20 30 40], len: 3, cap: 4 (Elemente von 20 bis 50 sind verfügbar) // Slices können niedrige und hohe Grenzen weglassen: s2 := arr[2:] // s2 == {30, 40, 50}, len: 3, cap: 3 s3 := arr[:3] // s3 == {10, 20, 30}, len: 3, cap: 5 s4 := arr[:] // s4 == {10, 20, 30, 40, 50}, len: 5, cap: 5 // Slicing eines anderen Slices: s5 := s1[1:] // s5 == {30, 40}, len: 2, cap: 3 (Elemente von s1 ab 30 bis 50 sind verfügbar)
Die Kapazität eines neuen Slices, der durch Slicing erstellt wird, wird durch die Kapazität des ursprünglichen Slices/Arrays abzüglich des low
-Index bestimmt. Dies stellt sicher, dass der neue Slice keine Elemente außerhalb der Grenzen des ursprünglichen Slices oder Arrays aufrufen kann.
2. Verwendung von make()
The make()
function is used to create slices with a specified length and an optional capacity. When make
creates a slice, it allocates a new underlying array in memory.
// Erstellen Sie einen Slice von ganzen Zahlen mit Länge 5 und Kapazität 5 // Alle Elemente werden auf ihren Nullwert initialisiert (0 für int) s := make([]int, 5) // s == {0, 0, 0, 0, 0}, len: 5, cap: 5 fmt.Printf("s: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // Erstellen Sie einen Slice von Strings mit Länge 3 und Kapazität 10 // Die zusätzliche Kapazität kann von append-Operationen ohne Neuallokation verwendet werden s2 := make([]string, 3, 10) // s2 == {"", "", ""}, len: 3, cap: 10 fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
3. Verwendung eines zusammengesetzten Literals
You can initialize a slice directly using a composite literal, similar to arrays, but without specifying the size. Go infers the length and capacity based on the provided elements. A new underlying array is allocated.
scores := []int{100, 95, 80, 75} // scores == {100, 95, 80, 75}, len: 4, cap: 4 fmt.Printf("scores: %v, len: %d, cap: %d\n", scores, len(scores), cap(scores)) names := []string{"Alice", "Bob", "Charlie"} // names == {"Alice", "Bob", "Charlie"}
Wesentliche Slice-Operationen
1. len(s)
: Aktuelle Länge
The len()
built-in function returns the number of elements currently in the slice. This is the "visible" size of the slice.
mySlice := []int{1, 2, 3, 4, 5} fmt.Println(len(mySlice)) // Ausgabe: 5 subSlice := mySlice[1:3] // {2, 3} fmt.Println(len(subSlice)) // Ausgabe: 2
2. cap(s)
: Zugrunde liegende Kapazität
The cap()
built-in function returns the capacity of the slice, which is the number of elements in the underlying array beginning from the slice's pointer that are available for the slice. This is crucial for understanding when the underlying array will be reallocated.
mySlice := []int{1, 2, 3, 4, 5} fmt.Println(cap(mySlice)) // Ausgabe: 5 (initial, len == cap for literal) subSlice := mySlice[1:3] // {2, 3} fmt.Println(cap(subSlice)) // Ausgabe: 4 (Elemente von Index 1 bis 4 des Arrays von mySlice) anotherSlice := make([]int, 2, 10) // len:2, cap:10 fmt.Println(len(anotherSlice), cap(anotherSlice)) // Ausgabe: 2 10
3. append(s, elems...)
: Elemente hinzufügen
The append()
built-in function is the primary way to add new elements to a slice. It conceptually returns a new slice containing the original elements plus the new ones. There are two scenarios:
- Ausreichende Kapazität: Wenn die Kapazität des Slices ausreicht, um die neuen Elemente aufzunehmen, erweitert
append
einfach die Länge des Slices und modifiziert das vorhandene zugrunde liegende Array. Der zurückgegebene Slice wird wahrscheinlich auf dasselbe zugrunde liegende Array verweisen, aber mit einer aktualisierten Länge. - Unzureichende Kapazität: Wenn nicht genügend Kapazität vorhanden ist, weist
append
ein neues, größeres zugrunde liegendes Array zu. Es kopiert alle vorhandenen Elemente vom alten Array in das neue, fügt die neuen Elemente hinzu und gibt dann einen Slice zurück, der auf dieses neue Array verweist. Das alte zugrunde liegende Array (und der Slice, der darauf verweist) wird für die Müllabfuhr in Frage gestellt, wenn keine anderen Referenzen existieren.
Go's Wachstumsstrategie für zugrunde liegende Arrays verdoppelt typischerweise die Kapazität, wenn eine Neuallokation erforderlich ist, bis zu einem bestimmten Schwellenwert, dann ein kleinerer Faktor (z. B. 1,25x) für sehr große Slices. Dies amortisiert die Kosten von Neuallokationen.
var numbers []int // nil slice, len:0, cap:0 numbers = append(numbers, 10) // numbers: [10], len:1, cap:1 (neues zugrunde liegendes Array) fmt.Printf("Nach 10: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers)) numbers = append(numbers, 20) // numbers: [10 20], len:2, cap:2 (neues zugrunde liegendes Array, normalerweise verdoppelt) fmt.Printf("Nach 20: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers)) numbers = append(numbers, 30, 40) // numbers: [10 20 30 40], len:4, cap:4 (wieder neues zugrunde liegendes Array) fmt.Printf("Nach 30, 40: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers)) // Das Anhängen eines Slices an einen anderen erfordert '...' moreNumbers := []int{50, 60} numbers = append(numbers, moreNumbers...) // numbers: [10 20 30 40 50 60] fmt.Printf("Nach Anhängen eines Slices: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers))
Wichtiger Hinweis zu append
: Da append
einen neuen Slice zurückgeben kann (der auf ein anderes zugrunde liegendes Array verweist), ist es entscheidend, seinen Rückgabewert dem ursprünglichen Slice-Variablen wieder zuzuweisen. Wenn Sie dies nicht tun, bleibt der ursprüngliche Slice unverändert (oder verweist auf das alte, potenziell volle zugrunde liegende Array).
s := []int{0, 1, 2} fmt.Printf("s vor append: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // s vor append: [0 1 2], len: 3, cap: 3 // Dieser Append weist neu zu und gibt einen NEUEN Slice zurück, aber `s` verweist immer noch auf den alten. append(s, 3, 4) fmt.Printf("s nach NICHT ZUGESCHRIEBENEM append: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // s nach NICHT ZUGESCHRIEBENEM append: [0 1 2], len: 3, cap: 3 // Korrekte Methode: Das Ergebnis zurückweisen s = append(s, 3, 4) fmt.Printf("s nach ZUGESCHRIEBENEM append: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // s nach ZUGESCHRIEBENEM append: [0 1 2 3 4], len: 5, cap: 6 (oder 8, je nach Go-Version/Architektur)
4. copy(dst, src)
: Elemente kopieren
The copy()
built-in function copies elements from a source slice (src
) to a destination slice (dst
). It copies min(len(src), len(dst))
elements. copy
does not allocate a new underlying array; it operates on existing arrays.
source := []int{10, 20, 30, 40, 50} destination := make([]int, 3) // destination: {0, 0, 0} n := copy(destination, source) // Kopiert 3 Elemente (10, 20, 30) von source nach destination fmt.Printf("Kopiert %d Elemente\n", n) // Ausgabe: Kopiert 3 Elemente fmt.Printf("source: %v\n", source) // source: [10 20 30 40 50] fmt.Printf("destination: %v\n", destination) // destination: [10 20 30] // Kopieren von mehr als dem in source Verfügbaren oder weniger als destination aufnehmen kann destination2 := make([]int, 10) copy(destination2, source) // Kopiert alle 5 Elemente von source nach destination2 fmt.Printf("destination2: %v\n", destination2) // destination2: [10 20 30 40 50 0 0 0 0 0] // Selbstkopieren (kann für In-Place-Änderungen wie Verschieben verwendet werden) s := []int{1, 2, 3, 4, 5} copy(s[1:], s[0:]) // Verschiebt Elemente: s[1] erhält s[0], s[2] erhält s[1], usw. fmt.Printf("Verschobener Slice: %v\n", s) // Verschobener Slice: [1 1 2 3 4]
copy
wird häufig verwendet für:
- Erstellung einer echten unabhängigen Kopie der Slice-Daten.
- Implementierung benutzerdefinierter Slice-Manipulationen (z. B. Einfügen, Löschen, Filtern).
Slice-Fallstricke und Best Practices
1. Modifizieren von zugrunde liegenden Arrays
Da Slices Ansichten sind, wirkt sich die Änderung eines Elements über einen Slice auf alle anderen Slices aus, die dasselbe zugrunde liegende Array gemeinsam nutzen, vorausgesetzt, die Änderung fällt in ihre jeweiligen Ansichten.
arr := [5]int{1, 2, 3, 4, 5} s1 := arr[1:4] // s1 == {2, 3, 4} s2 := arr[2:5] // s2 == {3, 4, 5} fmt.Printf("Initial: s1=%v, s2=%v, arr=%v\n", s1, s2, arr) s1[1] = 99 // Dies modifiziert arr[2] fmt.Printf("Nach s1[1]=99: s1=%v, s2=%v, arr=%v\n", s1, s2, arr) // Ausgabe: // Initial: s1=[2 3 4], s2=[3 4 5], arr=[1 2 3 4 5] // Nach s1[1]=99: s1=[2 99 4], s2=[99 4 5], arr=[1 2 99 4 5]
Dieses Verhalten ist normalerweise aus Effizienzgründen erwünscht, erfordert jedoch sorgfältiges Nachdenken, um unbeabsichtigte Nebenwirkungen zu vermeiden. Wenn Sie eine vollständig unabhängige Kopie benötigen, verwenden Sie append
oder copy
.
original := []int{1, 2, 3} // Erstellen Sie eine wirklich unabhängige Kopie independentCopy := make([]int, len(original), cap(original)) copy(independentCopy, original) independentCopy[0] = 99 fmt.Printf("Original: %v, Unabhängige Kopie: %v\n", original, independentCopy) // Ausgabe: Original: [1 2 3], Unabhängige Kopie: [99 2 3]
2. „Speicherlecks“ mit Teil-Slices
A common concern is that if you take a small sub-slice from a very large underlying array and only keep the sub-slice, the original large array might not be garbage collected because the sub-slice still holds a reference to it. This can lead to keeping more memory than strictly necessary.
func createBigSlice() []byte { bigData := make([]byte, 1<<20) // 1MB Slice // ... populate bigData ... return bigData[500:510] // Gib einen kleinen Slice aus der Mitte zurück } // Das zugrunde liegende 1MB-Array bleibt im Speicher, solange der 10-Byte-Slice, der von createBigSlice() zurückgegeben wird, erreichbar ist.
Um dies zu verhindern, erstellen Sie eine neue zugrunde liegende Array für Ihren kleineren Slice mit copy
, wenn Sie nur einen kleinen Teil eines großen Slices benötigen und möchten, dass der Rest garbage collected wird:
func createSmallIndependentSlice(bigData []byte) []byte { smallSlice := bigData[500:510] // Erstellen Sie einen neuen Slice mit seinem eigenen zugrunde liegenden Array independentSmallSlice := make([]byte, len(smallSlice)) copy(independentSmallSlice, smallSlice) return independentSmallSlice } // Jetzt kann der 'bigData'-Slice garbage collected werden, wenn keine anderen Referenzen existieren.
3. Nil-Slices vs. leere Slices
- Nil-Slice:
var s []int
oders := []int(nil)
. Er hatlen == 0
undcap == 0
. Es ist sicher,len
,cap
,append
undrange
auf einem Nil-Slice aufzurufen. - Leerer Slice:
s := []int{}
oders := make([]int, 0))
. Er hat ebenfallslen == 0
undcap == 0
(für[]int{}
) oder eine angegebene Kapazität (fürmake
).
Obwohl in vielen Kontexten funktional ähnlich, repräsentiert ein Nil-Slice wirklich „kein zugrunde liegendes Array“, während ein leerer Slice auf ein Array mit null Länge verweisen kann. Es ist im Allgemeinen gute Praxis, Nil-Slices für den „Nullwert“ eines Slices zu verwenden, da append
sie korrekt behandelt.
var nilSlice []int emptySlice := []int{} madeEmptySlice := make([]int, 0) fmt.Printf("nilSlice: %v, len: %d, cap: %d, ist nil: %t\n", nilSlice, len(nilSlice), cap(nilSlice), nilSlice == nil) fmt.Printf("emptySlice: %v, len: %d, cap: %d, ist nil: %t\n", emptySlice, len(emptySlice), cap(emptySlice), emptySlice == nil) fmt.Printf("madeEmptySlice: %v, len: %d, cap: %d, ist nil: %t\n", madeEmptySlice, len(madeEmptySlice), cap(madeEmptySlice), madeEmptySlice == nil) // Alle sind sicher, an sie anzuhängen nilSlice = append(nilSlice, 1) fmt.Printf("nilSlice nach append: %v\n", nilSlice) // Ausgabe: [1]
Schlussfolgerung
Go's Slices sind ein grundlegender und hochoptimierter Datentyp für die Arbeit mit Elementsequenzen. Durch das Verständnis ihres zugrunde liegenden Array-Modells und der Semantik von len
, cap
, append
und copy
erhalten Sie ein leistungsstarkes Werkzeug zum Erstellen effizienter und prägnanter Go-Programme. Denken Sie immer daran, dass Slices Ansichten sind, und dieses Kernprinzip wird Sie leiten, ihr Verhalten vorherzusagen und gängige Fallstricke zu vermeiden. Das Beherrschen von Slices ist ein bedeutender Schritt, um ein versierter Go-Entwickler zu werden.