Ein Slice in einem Beispiel
Ein Slice ist eine wachsende Sicht auf ein Array. Du erzeugst einen mit einem Literal oder mit make, lässt ihn mit append wachsen und liest seine Größe mit len.
[]string hat keine Länge in den Klammern. Das ist der Unterschied zu einem Array-Typ wie [4]string, dessen Größe feststeht.
Was ein Slice ist
Ein Slice-Wert ist ein kleiner Header mit drei Feldern:
- einem Pointer auf ein Element in einem Backing-Array
- einer Länge: wie viele Elemente der Slice sehen kann
- einer Kapazität: wie viele Elemente von diesem Pointer bis zum Ende des Backing-Arrays existieren
Einen Slice zu kopieren (zuweisen, an eine Funktion übergeben) kopiert nur diesen Header. Beide Kopien zeigen auf dieselben Elemente. Fast jede Überraschung mit Slices kommt daher, dass man das vergisst.
make: Länge und Kapazität
make([]T, len, cap) legt ein Backing-Array an und gibt einen Slice darüber zurück. Die Kapazität ist optional und entspricht standardmäßig der Länge.
Eine häufige Verwechslung: make([]int, 5) gefolgt von fünf Aufrufen von append ergibt zehn Elemente, die ersten fünf davon null. Nimm make([]int, 0, 5), wenn du anhängen willst, oder make([]int, 5), wenn du per Index zuweisen willst.
Kapazität vorab zu reservieren, wenn du die endgültige Größe kennst, vermeidet wiederholtes Wachsen. Das ist eine Optimierung, keine Pflicht.
append und Wachstum
append fügt Elemente hinter der aktuellen Länge an. Reicht die Kapazität, schreibt es direkt hinein. Wenn nicht, legt es ein größeres Array an, kopiert die alten Elemente hinüber und gibt einen Slice zurück, der auf das neue Array zeigt.
Die Kapazität verdoppelt sich ungefähr, solange der Slice klein ist, und wächst ab 256 Elementen um einen kleineren Faktor (in Richtung 1,25x). Die genauen Zahlen sind ein Detail der Runtime, das sich zwischen Go-Versionen geändert hat, also schreib nie Code, der davon abhängt.
Weil append ein anderes Array zurückgeben kann, musst du sein Ergebnis verwenden:
append(s, 4) // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct
Einen ganzen Slice hängst du mit der Spread-Syntax an: s = append(s, other...).
Slicing teilt das Backing-Array
s[low:high] erzeugt einen neuen Slice-Header über demselben Array, der die Indizes von low bis ausschließlich high abdeckt. Jede Grenze kann weggelassen werden. Es werden keine Elemente kopiert.
Ausgabe:
[10 99 30 40 50]
3 4
Dieses Teilen macht Slicing billig. Es ist aber auch die Quelle des berühmtesten Slice-Bugs.
Der Aliasing-Bug bei append
Hat ein Teil-Slice freie Kapazität, schreibt ein append darauf in das Array des Eltern-Slices, über Elemente, die der Eltern-Slice noch benutzt.
Ausgabe:
[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]
a und b teilen sich den Platz im Array nach "cmd", also hat das zweite append das erste überschrieben. Der Code sieht gut aus und funktioniert, solange die Kapazität zufällig voll ist. Deshalb taucht dieser Bug nur manchmal auf.
Zwei Lösungen:
- Der volle Slice-Ausdruck
s[low:high:max]begrenzt die Kapazität aufmax-low. Ohne freie Kapazität muss das nächsteappendneu anlegen.first := base[:2:2]macht das erste Beispiel sicher. - Explizit kopieren, wenn ein Slice den Aufruf überlebt oder unabhängig erweitert wird:
slices.Clone(prefix)oderappend([]string(nil), prefix...).
Eine verwandte Falle: Ein kleiner Slice eines riesigen Arrays hält für den Garbage Collector das ganze Array am Leben. Liest du eine Datei mit 100 MB und behältst data[:10], bleiben die 100 MB im Speicher. Kopier den Teil, den du behältst, mit Clone.
copy
Die eingebaute Funktion copy(dst, src) kopiert min(len(dst), len(src)) Elemente und gibt diese Anzahl zurück. Sie lässt dst nie wachsen.
copy behandelt überlappende Quelle und Ziel korrekt, also verschiebt copy(s[1:], s) Elemente nach rechts, ohne etwas zu beschädigen.
Das Paket slices
Seit Go 1.21 bietet das Standardpaket slices generische Helfer für die Operationen, die du früher von Hand geschrieben hast.
| Funktion | Was sie tut |
|---|---|
Contains, Index | einen Wert finden (ContainsFunc, IndexFunc nehmen ein Prädikat) |
Sort, SortFunc, SortStableFunc | direkt im Slice sortieren |
BinarySearch | einen sortierten Slice durchsuchen |
Insert, Delete, DeleteFunc | einfügen oder entfernen, geben den neuen Slice zurück |
Compact | aufeinanderfolgende Duplikate entfernen |
Equal, Compare | Element für Element vergleichen |
Clone, Reverse, Max, Min | das Naheliegende |
Collect, Sorted, Values, All | mit Iteratoren arbeiten (Go 1.23) |
Delete und Insert geben einen Slice zurück, den du zuweisen musst, wie bei append. Sortieren hat eine eigene Seite: Sortieren.
Wer beim Durchlaufen per Index Elemente entfernt, überspringt Elemente. Nimm stattdessen slices.DeleteFunc:
s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens
2D-Slices
Ein Slice von Slices ergibt ein Raster, dessen Zeilen unterschiedlich lang sein können. Jede Zeile muss separat angelegt werden.
Vergisst du das innere make, bleibt jede Zeile nil, und grid[1][2] = 7 löst eine Panic mit index out of range [2] with length 0 aus.
nil-Slices und leere Slices
Ein nil-Slice verhält sich bei len, cap, range und append wie ein leerer, also nimm var s []T als Nullzustand. Initialisier mit []T{} nur, wenn der Unterschied zählt, und das ist vor allem bei JSON-Ausgaben der Fall (null vs []). Prüf Leere mit len(s) == 0, nicht mit s == nil.
Slices und Funktionen
Eine Funktion, die einen Slice bekommt, kann seine Elemente ändern, und der Aufrufer sieht die Änderungen. Die Länge beim Aufrufer kann sie nicht ändern, weil sie eine Kopie des Headers bekommen hat. Eine Funktion, die anhängt, muss den neuen Slice zurückgeben:
func addAll(s []int, vals ...int) []int {
return append(s, vals...)
}
Deshalb geben append, slices.Delete und slices.Insert alle einen Slice zurück.
Häufige Fehler
- Das Ergebnis von
appendignorieren. Wird es ganz verworfen, kompiliert der Code nicht, aberappend(s, x)einer anderen Variablen zuzuweisen als der, die du weiter benutzt, ist ein Logikfehler. - Zwei appends von derselben Basis. Sie können sich den Speicher teilen. Kopier die Basis mit Clone oder nimm einen vollen Slice-Ausdruck.
make([]T, n)und dannappend. Lässt n Nullen am Anfang stehen.- Index außerhalb des Bereichs.
s[len(s)]löst eine Panic aus. Das letzte Element ists[len(s)-1]. - Slices mit
==vergleichen. Nurs == nilkompiliert. Nimmslices.Equal.
Häufig gestellte Fragen
Was ist der Unterschied zwischen Länge und Kapazität eines Slices in Go?
len(s) ist die Anzahl der Elemente, die der Slice gerade enthält. cap(s) ist die Anzahl der Elemente, die ab dem ersten Element des Slices in das Backing-Array passen. append schreibt in die freie Kapazität, wenn es welche gibt, und legt ein neues, größeres Array an, wenn nicht.
Wie hängt man in Go etwas an einen Slice an?
Ruf die eingebaute Funktion append auf und weise das Ergebnis zurück: s = append(s, x). Sie kann mehrere Werte (append(s, 1, 2, 3)) oder einen anderen Slice (append(s, other...)) anhängen. Nimm immer den Rückgabewert, weil append einen Slice zurückgeben kann, der auf ein neues Array zeigt.
Wie entferne ich in Go ein Element aus einem Slice?
Mit slices.Delete(s, i, i+1) (Go 1.21), das die späteren Elemente nach vorne schiebt und den kürzeren Slice zurückgibt. Seit Go 1.22 nullt es außerdem die frei gewordenen Plätze am Ende. Ist die Reihenfolge egal, tausch mit dem letzten Element und kürze: s[i] = s[len(s)-1]; s = s[:len(s)-1].
Wie prüfe ich in Go, ob ein Slice einen Wert enthält?
Mit slices.Contains(s, v) aus dem Standardpaket slices (Go 1.21). slices.Index(s, v) gibt die Position oder -1 zurück, und slices.ContainsFunc nimmt ein Prädikat. Für wiederholte Lookups in großen Daten baust du stattdessen eine Map.
Was ist der Unterschied zwischen einem nil-Slice und einem leeren Slice in Go?
var s []int ist nil; s := []int{} ist leer, aber nicht nil. Beide haben die Länge 0, beide funktionieren mit len, range und append. Der Unterschied zeigt sich bei Prüfungen mit s == nil und in encoding/json, das einen nil-Slice als null und einen leeren Slice als [] kodiert.