Menu

Golang Slice: append, make, len vs cap und der Sharing-Bug

Slices sind der alltägliche Listentyp von Go. make, append und Wachstum, len vs cap, wie Slicing das Backing-Array teilt (und welchen Bug das verursacht), copy, das Paket slices, 2D-Slices und nil vs leer.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

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:

  1. Der volle Slice-Ausdruck s[low:high:max] begrenzt die Kapazität auf max-low. Ohne freie Kapazität muss das nächste append neu anlegen. first := base[:2:2] macht das erste Beispiel sicher.
  2. Explizit kopieren, wenn ein Slice den Aufruf überlebt oder unabhängig erweitert wird: slices.Clone(prefix) oder append([]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.

FunktionWas sie tut
Contains, Indexeinen Wert finden (ContainsFunc, IndexFunc nehmen ein Prädikat)
Sort, SortFunc, SortStableFuncdirekt im Slice sortieren
BinarySearcheinen sortierten Slice durchsuchen
Insert, Delete, DeleteFunceinfügen oder entfernen, geben den neuen Slice zurück
Compactaufeinanderfolgende Duplikate entfernen
Equal, CompareElement für Element vergleichen
Clone, Reverse, Max, Mindas Naheliegende
Collect, Sorted, Values, Allmit 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 append ignorieren. Wird es ganz verworfen, kompiliert der Code nicht, aber append(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 dann append. Lässt n Nullen am Anfang stehen.
  • Index außerhalb des Bereichs. s[len(s)] löst eine Panic aus. Das letzte Element ist s[len(s)-1].
  • Slices mit == vergleichen. Nur s == nil kompiliert. Nimm slices.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.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S