Der Slice ist eine flexible Datenstruktur in Go, die eine dynamisch veränderbare Sequenz von Elementen desselben Typs ermöglicht. Im Gegensatz zu Arrays ist die Größe eines Slice nicht festgelegt, sondern kann zur Laufzeit angepasst werden. Slices bieten effizienten Zugriff, einfache Erweiterbarkeit und sind ein zentrales Werkzeug für die Arbeit mit Datenmengen in Go.

Einführung

Slices stellen eine flexible, dynamische Sicht auf Arrays dar und sind das bevorzugte Mittel, um mit Sequenzen von Elementen zu arbeiten. Im Gegensatz zu Arrays, die eine feste Größe haben, bieten Slices eine elegante Möglichkeit, mit Sammlungen variabler Länge zu arbeiten.

Ein Slice ist im Wesentlichen ein Descriptor, der einen Ausschnitt eines darunterliegenden Arrays beschreibt. Es handelt sich um einen Referenztyp, der drei Komponenten enthält: einen Zeiger auf das zugrunde liegende Array, die Länge und die Kapazität. Diese Struktur macht Slices sowohl leistungsfähig als auch effizient, da beim Übergeben eines Slices an eine Funktion nicht das gesamte darunterliegende Array kopiert wird, sondern nur der Descriptor selbst.

Die Slices in Go werden überall in der Go-Standardbibliothek verwendet und sind das idiomatische Mittel zur Arbeit mit Listen und Sequenzen. Während Arrays in Go ihren Platz haben, sind Slices deutlich flexibler und werden weitaus häufiger verwendet.

Unterschied zwischen Slice und Array in Go

Um Slices vollständig zu verstehen, ist es wichtig, den Unterschied zu Arrays zu kennen. Arrays in Go haben eine feste Größe, die Teil ihres Typs ist. Das bedeutet, dass ein Array vom Typ [5]int ein völlig anderer Typ ist als ein Array vom Typ [10]int. Diese Größenbeschränkung macht Arrays unflexibel für viele praktische Anwendungen.

Go Beispiel - Arrays mit fester Größe
package main

import "fmt"

func main() {
    var numArr [5]int
    numArr[0] = 10
    numArr[1] = 20
    numArr[2] = 30

    fmt.Printf("Array: %v\n", numArr)
    fmt.Printf("Länge: %d\n", len(numArr))
}
Output
Array: [10 20 30 0 0]
Länge: 5

Slices hingegen haben keine feste Größe als Teil ihres Typs. Ein Slice vom Typ []int kann beliebig viele Integer-Werte enthalten. Diese Flexibilität macht Slices zur bevorzugten Wahl für die meisten Anwendungsfälle.

Go Beispiel - Slice mit variabler Größe
package main

import "fmt"

func main() {
    var numSlice []int
    numSlice = append(numSlice, 10)
    numSlice = append(numSlice, 20)
    numSlice = append(numSlice, 30)

    fmt.Printf("Slice: %v\n", numSlice)
    fmt.Printf("Länge: %d\n", len(numSlice))

    // Slices sind flexibel erweiterbar
    numSlice = append(numSlice, 40, 50, 60)
    fmt.Printf("Slice erweitert: %v\n", numSlice)
}
Output
Slice: [10 20 30]
Länge: 3
Slice erweitert: [10 20 30 40 50 60]

Ein weiterer wichtiger Unterschied besteht darin, wie Arrays und Slices an Funktionen übergeben werden.

Arrays werden als Wert kopiert, was bei großen Arrays ineffizient sein kann. Slices hingegen werden als Referenzen übergeben (genauer gesagt wird der Slice-Descriptor kopiert, der nur wenige Bytes groß ist), wodurch sie deutlich effizienter sind.

Go Beispiel - Übergabe an Funktion
package main

import "fmt"

func modifyArray(arr [3]int) {
    arr[0] = 999
}

func modifySlice(s []int) {
    s[0] = 999
}

func main() {

    // Array-Beispiel
    myArr := [3]int{1, 2, 3}
    fmt.Printf("Array vorher: %v\n", myArr)

    modifyArray(myArr)
    fmt.Printf("Array nachher: %v\n", myArr)

    fmt.Println("--- --- ---")

    // Slice-Beispiel
    mySlice := []int{1, 2, 3}
    fmt.Printf("Slice vorher: %v\n", mySlice)

    modifySlice(mySlice)
    fmt.Printf("Slice nachher: %v\n", mySlice)

}
Output
Array vorher: [1 2 3]
Array nachher: [1 2 3]
--- --- ---
Slice vorher: [1 2 3]
Slice nachher: [999 2 3]

Wie man in diesem Beispiel sehen kann, wirkt sich die Änderung des Slices in einer Funktion direkt auf das ursprüngliche Slice aus, während beim Array nur die Kopie innerhalb der Funktion modifiziert wurde.

Interne Struktur von Slices

Ein tieferes Verständnis der internen Struktur von Slices ist entscheidend, um ihre Funktionsweise und ihr Verhalten vollständig zu verstehen.

Intern ist ein Slice eine Datenstruktur, die drei Komponenten enthält:

  1. Pointer (Zeiger): Ein Zeiger auf das erste Element des Slices im darunterliegenden Array.
  2. Length (Länge): Die Anzahl der Elemente im Slice.
  3. Capacity (Kapazität): Die maximale Anzahl von Elementen, die das Slice aufnehmen kann, bevor eine Neu-Allokation erforderlich ist.
Go Beispiel - Länge und Kapazität
package main

import "fmt"

func main() {
    // Ein Slice mit make erstellen
    s := make([]int, 5, 10)

    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))
}
Output
Slice: [0 0 0 0 0]
Länge: 5
Kapazität: 10

Der Unterschied zwischen Länge und Kapazität ist fundamental. Die Länge gibt die aktuelle Größe des Slices an - also wie viele Elemente tatsächlich darauf zugreifbar sind. Die Kapazität hingegen zeigt, wie viel “Puffer” im darunterliegenden Array vorhanden ist.

Wenn man Elemente zu einem Slice hinzufügt und die Kapazität ausgeschöpft ist, muss Go ein neues, größeres Array allozieren und die Daten kopieren.

Im folgenden Beispiel wird das Wachsen der Kapazität eines Slices gezeigt.

Go Beispiel - Wachsen der Kapazität
package main

import "fmt"

func main() {
    var s []int
    fmt.Printf("Initiale Länge: %d\n", len(s))
    fmt.Printf("Initiale Kapazität: %d\n", cap(s))

    for i := 0; i < 20; i++ {
        s = append(s, i)
        fmt.Printf("Durchlauf %d | Länge: %d | Kapazität: %d\n", i, len(s), cap(s))
    }
}
Output
Initiale Länge: 0
Initiale Kapazität: 0
Durchlauf 0 | Länge: 1 | Kapazität: 4
Durchlauf 1 | Länge: 2 | Kapazität: 4
Durchlauf 2 | Länge: 3 | Kapazität: 4
Durchlauf 3 | Länge: 4 | Kapazität: 4
Durchlauf 4 | Länge: 5 | Kapazität: 8
Durchlauf 5 | Länge: 6 | Kapazität: 8
Durchlauf 6 | Länge: 7 | Kapazität: 8
Durchlauf 7 | Länge: 8 | Kapazität: 8
Durchlauf 8 | Länge: 9 | Kapazität: 16
Durchlauf 9 | Länge: 10 | Kapazität: 16
Durchlauf 10 | Länge: 11 | Kapazität: 16
Durchlauf 11 | Länge: 12 | Kapazität: 16
Durchlauf 12 | Länge: 13 | Kapazität: 16
Durchlauf 13 | Länge: 14 | Kapazität: 16
Durchlauf 14 | Länge: 15 | Kapazität: 16
Durchlauf 15 | Länge: 16 | Kapazität: 16
Durchlauf 16 | Länge: 17 | Kapazität: 32
Durchlauf 17 | Länge: 18 | Kapazität: 32
Durchlauf 18 | Länge: 19 | Kapazität: 32
Durchlauf 19 | Länge: 20 | Kapazität: 32

An diesem Beispiel sieht man, dass die Kapazität in Schritten wächst. Go verdoppelt die Kapazität bei Bedarf.

Um zu beweisen, dass Go das zugrunde liegende Array eines Slices austauscht, wenn die Kapazität erhöht werden muss, können wir uns den Speicherplatz des ersten Elements im Array immer mit ausgeben.

Go Beispiel - Wachsen der Kapazität
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var s []int
    fmt.Printf("Initiale Länge: %d\n", len(s))
    fmt.Printf("Initiale Kapazität: %d\n", cap(s))

    for i := 0; i < 20; i++ {
        s = append(s, i)
        fmt.Printf("Durchlauf %d | Länge: %d | Kapazität: %d\n", i, len(s), cap(s))
        fmt.Printf("Speicher (erstes Element): %p\n", unsafe.Pointer(&s[0]))
    }
}
Output
Initiale Länge: 0
Initiale Kapazität: 0
Durchlauf 0 | Länge: 1 | Kapazität: 1
Speicher (erstes Element): 0x14000094020
Durchlauf 1 | Länge: 2 | Kapazität: 2
Speicher (erstes Element): 0x14000094030
Durchlauf 2 | Länge: 3 | Kapazität: 4
Speicher (erstes Element): 0x140000bc000
Durchlauf 3 | Länge: 4 | Kapazität: 4
Speicher (erstes Element): 0x140000bc000
Durchlauf 4 | Länge: 5 | Kapazität: 8
Speicher (erstes Element): 0x140000a8040
Durchlauf 5 | Länge: 6 | Kapazität: 8
Speicher (erstes Element): 0x140000a8040
Durchlauf 6 | Länge: 7 | Kapazität: 8
Speicher (erstes Element): 0x140000a8040
Durchlauf 7 | Länge: 8 | Kapazität: 8
Speicher (erstes Element): 0x140000a8040
Durchlauf 8 | Länge: 9 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 9 | Länge: 10 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 10 | Länge: 11 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 11 | Länge: 12 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 12 | Länge: 13 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 13 | Länge: 14 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 14 | Länge: 15 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 15 | Länge: 16 | Kapazität: 16
Speicher (erstes Element): 0x140000be000
Durchlauf 16 | Länge: 17 | Kapazität: 32
Speicher (erstes Element): 0x140000c0000
Durchlauf 17 | Länge: 18 | Kapazität: 32
Speicher (erstes Element): 0x140000c0000
Durchlauf 18 | Länge: 19 | Kapazität: 32
Speicher (erstes Element): 0x140000c0000
Durchlauf 19 | Länge: 20 | Kapazität: 32
Speicher (erstes Element): 0x140000c0000

Dieses Beispiel zeigt ganz schön, dass die Kennung des Speicherplatzes im Speicher synchron mit der Erhöhung der Kapazität sich ändert. Dies beweist, dass Go das Array “unter der Haube” gegen ein größeres austauscht.

Erstellen von Slices

Es gibt mehrere Möglichkeiten, Slices in Go zu erstellen. Jede Methode hat ihre eigenen Anwendungsfälle und Eigenschaften. Die Wahl der richtigen Methode hängt vom Kontext und den Anforderungen ab.

Slice Literals

Die einfachste Methode ist die Verwendung von Slice Literals. Diese Syntax ähnelt Array Literals, aber ohne Größenangabe in den eckigen Klammern. Slice Literals sind ideal, wenn man bereits weiß, welche Werte das Slice enthalten soll.

Go Beispiel
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3, 4}

    fmt.Printf("Slice: %v\n", nums)
    fmt.Printf("Länge: %d\n", len(nums))
    fmt.Printf("Kapazität: %d\n", cap(nums))

    fmt.Println()

    fruits := []string{"Apfel", "Birne", "Orange"}
    fmt.Printf("Früchte: %v\n", fruits)
}
Output
Slice: [1 2 3 4]
Länge: 4
Kapazität: 4

Früchte: [Apfel Birne Orange]

Die make Funktion

Die make Funktion ist die idiomatische Art, Slices mit einer bestimmten Länge und optional mit einer bestimmten Kapazität zu erstellen. Diese Methode ist besonders nützlich, wenn man ein Slice mit einer bestimmten Größe benötigt und die Werte später füllen möchte.

Go Beispiel - make
package main

import "fmt"

func main() {
    // Slice mit Länge 5
    // Kapazität wird automatisch auf 5 gesetzt
    s := make([]int, 5)

    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))

    // Bestimmte Elemente ändern
    s[0] = 100
    s[2] = 200
    fmt.Printf("Modifiziert: %v\n", s)
}
Output
Slice: [0 0 0 0 0]
Länge: 5
Kapazität: 5
Modifiziert: [100 200 0 0 0]

Abweichende Kapazität

Man kann auch explizit eine Kapazität angeben, die größer als die Länge ist. Dies ist sinnvoll, wenn man weiß, dass das Slice wachsen wird und man Reallokationen vermeiden möchte.

Go Beispiel - Länge & Kapazität
package main

import "fmt"

func main() {
    // Slice mit Länge 3 & Kapazität 10
    s := make([]int, 3, 10)

    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))

    // Man kann nur auf die ersten 3 Elemente zugreifen.
    s[0] = 10
    s[1] = 20
    s[2] = 30

    // s[3] => würde einen Fehler verursachen

    // Man kann allerdings ein Slice erweitern
    s = append(s, 40, 50)
    fmt.Printf("Nach Erweiterung: %v\n", s)
    fmt.Printf("Neue Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))

}
Output
Slice: [0 0 0]
Länge: 3
Kapazität: 10
Nach Erweiterung: [10 20 30 40 50]
Neue Länge: 5
Kapazität: 10

Von einem Array ableiten

Man kann ein Slice auch durch “Slicing” eines existierenden Arrays erstellen. Das Slice teilt sich dann den Speicher mit dem Array, was effizient ist, aber auch bedeutet, dass Änderungen am Slice das ursprüngliche Array beeinflussen und umgekehrt.

Go Beispiel - Slice aus Array
package main

import "fmt"

func main() {
    // Ein Array erstellen
    myArr := [5]int{1, 2, 3, 4, 5}

    // Ein Slice vom gesamten Array
    mySlice := myArr[:]
    fmt.Printf("Slice myArr[:]: %v\n", mySlice)

    // Ein Slice von einem Teil des Arrays
    myPartSlice := myArr[1:4] // 1, 2, 3 (4 exklusiv)
    fmt.Printf("Slice myArr[1:4]: %v\n", myPartSlice)

    // Änderungen am Slice beeinflussen das Array
    mySlice[0] = 999
    fmt.Printf("Array nach Slice-Änderung: %v\n", myArr)
}
Output
Slice myArr[:]: [1 2 3 4 5]
Slice myArr[1:4]: [2 3 4]
Array nach Slice-Änderung: [999 2 3 4 5]

Deklaration ohne Initialisierung

Man kann ein Slice auch ohne Initialisierung deklarieren. In diesem Fall ist das Slice ein nil slice mit der Länge 0 und Kapazität 0.

Go Beispiel - Ohne Initialisierung
package main

import "fmt"

func main() {
    var s []int

    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))

    // Man kann das Slice erweitern
    s = append(s, 1, 2, 3)
    fmt.Printf("Slice aktualisiert: %v\n", s)
    fmt.Printf("Länge aktualisiert: %d\n", len(s))
}
Output
Slice: []
Länge: 0
Kapazität: 0
Slice aktualisiert: [1 2 3]
Länge aktualisiert: 3

Slice Operationen

Slices unterstützen einige Operationen, die sie zu einem mächtigen Werkzeug für die Arbeit mit Datensequenzen machen. Die wichtigsten Operationen sind append, copy, Slicing und Iteration.

Die append Funktion

Die append Funktion ist eine Methode, um Elemente zu einem Slice hinzuzufügen. Sie ist so wichtig und häufig verwendet, dass sie als Built-in-Funktion in Go integriert ist. Die Funktion append nimmt ein Slice und ein oder mehrere Elemente entgegen und gibt ein neues Slice zurück, das die ursprünglichen und die neuen Elemente enthält.

Go Beispiel - append
package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    fmt.Printf("Original: %v\n", s)

    // Ein Element hinzufügen
    s = append(s, 4)
    fmt.Printf("Nach append(s, 4): %v\n", s)

    // Mehrere Elemente hinzufügen
    s = append(s, 5, 6, 7)
    fmt.Printf("Nach append(s, 5, 6, 7): %v\n", s)
}
Output
Original: [1 2 3]
Nach append(s, 4): [1 2 3 4]
Nach append(s, 5, 6, 7): [1 2 3 4 5 6 7]

Ein wichtiger Aspekt von append ist, dass es ein neues Slice zurückgibt. Wenn die Kapazität des ursprünglichen Slices ausreicht, wird das gleiche darunterliegende Array verwendet. Wenn nicht, wird ein neues Array alloziert. Daher ist es wichtig, das Ergebnis von append immer der Slice-Variable zuzuweisen.

Go Beispiel - mit Kapazität-Überschreitung
package main

import "fmt"

func main() {
    // Slice mit begrenzter Kapazität
    s := make([]int, 3, 5)
    s[0], s[1], s[2] = 1, 2, 3

    fmt.Printf("Initiales Slice: %v\n", s)
    fmt.Printf("Initiale Länge: %d\n", len(s))
    fmt.Printf("Initiale Kapazität: %d\n", cap(s))

    // Innerhalb der Kapazität
    s = append(s, 4)
    fmt.Println("\nNach append(4)")
    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))

    s = append(s, 5)
    fmt.Println("\nNach append(5)")
    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))

    // Überschreitung der Kapazität
    // Neues Array wird alloziert
    s = append(s, 6)
    fmt.Println("\nNach append(6)")
    fmt.Printf("Slice: %v\n", s)
    fmt.Printf("Länge: %d\n", len(s))
    fmt.Printf("Kapazität: %d\n", cap(s))
}
Output
Initiales Slice: [1 2 3]
Initiale Länge: 3
Initiale Kapazität: 5

Nach append(4)
Slice: [1 2 3 4]
Länge: 4
Kapazität: 5

Nach append(5)
Slice: [1 2 3 4 5]
Länge: 5
Kapazität: 5

Nach append(6)
Slice: [1 2 3 4 5 6]
Länge: 6
Kapazität: 10

Man kann auch ein komplettes Slice an ein anderes Slice anhängen, indem man den Spread-Operator ... verwendet.

Go Beispiel - Slices zusammenführen
package main

import "fmt"

func main() {
    sOne := []int{1, 2, 3}
    sTwo := []int{4, 5, 6}

    // sTwo an sOne anhängen
    sOne = append(sOne, sTwo...)

    fmt.Printf("Zusammengeführt: %v\n", sOne)
}
Output
Zusammengeführt: [1 2 3 4 5 6]

Die copy Funktion

Die copy Funktion kopiert Elemente von einem Quell-Slice zu einem Ziel-Slice. Im Gegensatz zu append modifiziert copy das Ziel-Slice direkt und gibt die Anzahl der kopierten Elemente zurück. Die Anzahl der kopierten Elemente ist das Minimum aus der Länge des Quell- und des Ziel-Slices.

Go Beispiel - Einfaches Kopieren
package main

import "fmt"

func main() {
    sourceSlice := []int{1, 2, 3, 4, 5}
    targetSlice := make([]int, 5)

    // Alle Elemente kopieren
    n := copy(targetSlice, sourceSlice)

    fmt.Printf("Quelle: %v\n", sourceSlice)
    fmt.Printf("Ziel: %v\n", targetSlice)
    fmt.Printf("Anzahl kopierter Elemente: %d\n", n)

    // Änderungen am Ziel beeinflussen die Quelle nicht
    targetSlice[0] = 999
    fmt.Printf("Quelle nach Änderung: %v\n", sourceSlice)
    fmt.Printf("Ziel nach Änderung: %v\n", targetSlice) 
}
Output
Quelle: [1 2 3 4 5]
Ziel: [1 2 3 4 5]
Anzahl kopierter Elemente: 5
Quelle nach Änderung: [1 2 3 4 5]
Ziel nach Änderung: [999 2 3 4 5]

Wenn das Ziel-Slice kleiner ist als das Quell-Slice, werden nur so viele Elemente kopiert, wie ins Ziel-Slice passen.

Go Beispiel - Teilweises Kopieren
package main

import "fmt"

func main() {
    sourceSlice := []int{1, 2, 3, 4, 5}
    targetSlice := make([]int, 3)

    n := copy(targetSlice, sourceSlice)

    fmt.Printf("Quelle: %v\n", sourceSlice)
    fmt.Printf("Ziel: %v\n", targetSlice)
    fmt.Printf("Anzahl kopierter Elemente: %d\n", n)
}
Output
Quelle: [1 2 3 4 5]
Ziel: [1 2 3]
Anzahl kopierter Elemente: 3

Ein interessanter Anwendungsfall ist das Kopieren innerhalb des gleichen Slices, zum Beispiel zum Entfernen von Elementen.

Go Beispiel - Element entfernen
package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    fmt.Printf("Original: %v\n", s)

    index := 2
    copy(s[index:], s[index+1:])
    s = s[:len(s)-1]

    fmt.Printf("Nach Entfernung: %v\n", s)
}
Output
Original: [1 2 3 4 5]
Nach Entfernung: [1 2 4 5]

Slicing - Teilausschnitt erstellen

Die Slicing-Syntax ermöglicht es, Teilausschnitte von Slices zu erstellen. Die Syntax ist slice[low:high], wobei low inklusiv und high exklusiv ist. Man kann auch low oder high weglassen, um vom Anfang bis zum Ende zu slicen.

Go Beispiel - Index 2 bis 5
package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("Original: %v\n", s)

    sliced := s[2:5]
    fmt.Printf("s[2:5]: %v\n", sliced)
}
Output
Original: [0 1 2 3 4 5 6 7 8 9]
s[2:5]: [2 3 4]

Im nächsten Beispiel wird gezeigt, wie man vom Anfang bis zum Index 4 slicen kann.

Go Beispiel - Anfang bis Index 4
package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("Original: %v\n", s)

    sliced := s[:4]
    fmt.Printf("s[:4]: %v\n", sliced)
}
Output
Original: [0 1 2 3 4 5 6 7 8 9]
s[:4]: [0 1 2 3]

Das folgende Beispiel zeigt, wie man vom Index 6 bis zum Ende des Slices slicen kann.

Go Beispiel - Index 6 zum Ende
package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Printf("Original: %v\n", s)

    sliced := s[6:]
    fmt.Printf("s[6:]: %v\n", sliced)
}
Output
Original: [0 1 2 3 4 5 6 7 8 9]
s[6:]: [6 7 8 9]

Ein wichtiger Punkt ist, dass Slices, die durch Slicing erstellt werden, das gleiche darunterliegende Array teilen. Änderungen an einem Slice wirken sich auf alle anderen Slices aus, die das gleiche Array referenzieren.

Go Beispiel - Speicher bei Slices
package main

import "fmt"

func main() {
    original := []int{0, 1, 2, 3, 4, 5}
    sliceOne := original[1:4]
    sliceTwo := original[2:5]

    fmt.Printf("Original: %v\n", original)
    fmt.Printf("sliceOne [1:4]: %v\n", sliceOne)
    fmt.Printf("sliceTwo [2:5]: %v\n", sliceTwo)

    // Änderung in sliceOne
    sliceOne[0] = 999

    fmt.Printf("\nNach sliceOne[0] = 999:\n")
    fmt.Printf("Original: %v\n", original)
    fmt.Printf("sliceOne [1:4]: %v\n", sliceOne)
    fmt.Printf("sliceTwo [2:5]: %v\n", sliceTwo)
}
Output
Original: [0 1 2 3 4 5]
sliceOne [1:4]: [1 2 3]
sliceTwo [2:5]: [2 3 4]

Nach sliceOne[0] = 999:
Original: [0 999 2 3 4 5]
sliceOne [1:4]: [999 2 3]
sliceTwo [2:5]: [2 3 4]

Man kann auch eine dritte Komponente in der Slicing-Syntax verwenden, um die Kapazität des neuen Slices zu beschränken: slice[low:high:max]. Dies wird als Full Slice Expression bezeichnet.

Go Beispiel - Full Slice Expression
package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

    // Normales Slicing
    sliceOne := s[2:5]
    fmt.Printf("--- s[2:5] ---\n")
    fmt.Printf("Länge: %d\n", len(sliceOne))
    fmt.Printf("Kapazität: %d\n", cap(sliceOne))
    fmt.Printf("Slice: %v\n", sliceOne)

    // Mit Kapazitätsbeschränkung
    sliceTwo := s[2:5:7]
    fmt.Printf("\n--- s[2:5:7] ---\n")
    fmt.Printf("Länge: %d\n", len(sliceTwo))
    fmt.Printf("Kapazität: %d\n", cap(sliceTwo))
    fmt.Printf("Slice: %v\n", sliceTwo)
}
Output
--- s[2:5] ---
Länge: 3
Kapazität: 8
Slice: [2 3 4]

--- s[2:5:7] ---
Länge: 3
Kapazität: 5
Slice: [2 3 4]

An diesem Punkt ist es vielleicht wichtig zu verstehen, wie die Kapazitäten 8 und 5 zustande kommen.

Berechnung der Länge und Kapazität beim normalen Slicing

Hier gilt folgende Formel.

Go Normales Slicing
s[low:high]

Länge = high - low
Kapazität = cap(s) - low

Beim oberen Beispiel würde es bedeuten:

Go Normales Slicing
Länge = 5 - 2 = 3
Kapazität = 10 - 2 = 8

Berechnung der Länge und Kapazität bei Full Slice Expression

Go Syntax
s[low:high:max]

Das dritte Argument max gibt an, wo die Kapazität enden soll.

Go berechnet dann:

Go
Länge = high - low
Kapazität = max - low

Das bedeutet dann in Bezug auf das obere Beispiel Folgendes:

Go
low = 2
high = 5
max = 7

Länge = 5 - 2 = 3
Kapazität = 7 - 2 = 5

Iteration über Slices

Es gibt mehrere Möglichkeiten, über Slices zu iterieren. Die idiomatische Methode ist die Verwendung einer for range Schleife, die sowohl den Index als auch den Wert liefert.

Hier ein Beispiel.

Go Beispiel - for range
package main

import "fmt"

func main() {
    nums := []int{10, 20, 30, 40, 50}

    // Index und Wert
    fmt.Println("Mit Index und Wert:")
    for i, value := range nums {
        fmt.Printf("Index: %d: %d\n", i, value)
    }

    // Nur Wert
    fmt.Println("\nNur Wert:")
    for _, value := range nums {
        fmt.Printf("%d ", value)
    }
    fmt.Println()

    // Nur Index
    fmt.Println("\nNur Index:")
    for i := range nums {
        fmt.Printf("%d ", i)
    }
    fmt.Println()
}
Output
Mit Index und Wert:
Index: 0: 10
Index: 1: 20
Index: 2: 30
Index: 3: 40
Index: 4: 50

Nur Wert:
10 20 30 40 50 

Nur Index:
0 1 2 3 4

Es besteht auch die Möglichkeit eine traditionelle for Schleife mit Indexzugriff zu verwenden, was manchmal nützlich ist, wenn man die Iterationsreihenfolge oder den Schritt kontrollieren möchte.

Go Beispiel - for Schleife
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Jedes zweite Element
    fmt.Println("Jedes zweite Element")
    for i := 0; i < len(nums); i += 2 {
        fmt.Printf("%d ", nums[i])
    }
    fmt.Println()

    // Rückwärts iterieren
    fmt.Println("Rückwärts:")
    for i := len(nums) - 1; i >= 0; i-- {
        fmt.Printf("%d ", nums[i])
    }
    fmt.Println()
}
Output
Jedes zweite Element
1 3 5 7 9 
Rückwärts:
10 9 8 7 6 5 4 3 2 1

Länge und Kapazität

Das Konzept von Länge (Length) und Kapazität (Capacity) ist wichtig für das Verständnis von Slices. Die Länge ist die Anzahl der Elemente, die das Slice aktuell enthält und auf die zugegriffen werden kann. Die Kapazität ist die Größe des darunterliegenden Arrays ab dem ersten Element des Slices.

Die Kapazität bestimmt, wie viele Elemente zum Slice hinzugefügt werden können, ohne dass ein neues Array alloziert werden muss. Wenn man die Kapazität eines Slices überschreitet, erstellt Go automatisch ein neues, größeres Array, kopiert die vorhandenen Daten hinein und aktualisiert den Slice-Descriptor.

Ein Beispiel nochmals zu Länge und Kapazität.

Go Beispiel - Länge und Kapazität
package main

import "fmt"

func main() {
    arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    slice := arr[2:5]

    fmt.Printf("Array: %v\n", arr)
    fmt.Printf("Slice s[2:5]: %v\n", slice)
    fmt.Printf("Länge slice: %d\n", len(slice))
    fmt.Printf("Kapazität slice: %d\n", cap(slice))
}
Output
Array: [0 1 2 3 4 5 6 7 8 9]
Slice s[2:5]: [2 3 4]
Länge slice: 3
Kapazität slice: 8

Das Kapazitätswachstum von Go folgt einer bestimmten Strategie, um die Anzahl der Reallokationen zu minimieren. Für kleinere Slices verdoppelt Go typischerweise die Kapazität, für größere Slices wächst die Kapazität um 25%.

Hier ein Beispiel, das das Wachstum der Kapazität verdeutlicht.

Go Beispiel - Kapazität Wachstum
package main

import "fmt"

func main() {
    s := make([]int, 0)
    prevCap := 0

    for i := 0; i < 1000; i++ {
        s = append(s, i)
        if cap(s) != prevCap {
            fmt.Printf("Länge: %3d\n", len(s))
            fmt.Printf("Kapazität: %3d (Wachstum von %d)\n", cap(s), cap(s)-prevCap)
            prevCap = cap(s)
        }
    }
}
Output
Länge:   1
Kapazität:   4 (Wachstum von 4)
Länge:   5
Kapazität:   8 (Wachstum von 4)
Länge:   9
Kapazität:  16 (Wachstum von 8)
Länge:  17
Kapazität:  32 (Wachstum von 16)
Länge:  33
Kapazität:  64 (Wachstum von 32)
Länge:  65
Kapazität: 128 (Wachstum von 64)
Länge: 129
Kapazität: 256 (Wachstum von 128)
Länge: 257
Kapazität: 512 (Wachstum von 256)
Länge: 513
Kapazität: 848 (Wachstum von 336)
Länge: 849
Kapazität: 1280 (Wachstum von 432)

Wenn man die Kapazität im Voraus kennt, sollte man sie bei der Erstellung des Slices angeben, um Reallokationen zu vermeiden. Dies ist eine wichtige Performance-Optimierung bei der Arbeit mit großen Datenmengen.

Go Beispiel - Bekannte Kapazität
package main

import "fmt"

func main() {

    // Ineffizient - Mehrere Reallokationen
    sOne := []int{}
    for i := 0; i < 1000; i++ {
        sOne = append(sOne, i)
    }

    // Effizient - Kapazität im Voraus reserviert
    sTwo := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        sTwo = append(sTwo, i)
    }

    fmt.Printf("Beide Slices haben 1000 Elemente\n")
    fmt.Printf("sOne - Länge = %d | Kapazität = %d\n", len(sOne), cap(sOne))
    fmt.Printf("sTwo - Länge = %d | Kapazität = %d\n", len(sTwo), cap(sTwo))
}

Häufige Stolperfallen

append kann das Underlying-Array teilen oder neu allokieren.

Reicht die Kapazität, schreibt append in das bestehende Array und gibt einen Slice mit gleichem Pointer zurück. Reicht sie nicht, wird ein neues Array alloziert. Dadurch beobachtest du in der einen Hälfte deines Codes geteilte Mutationen, in der anderen plötzlich nicht mehr — abhängig davon, ob cap gerade reicht. Deshalb gilt die Regel: das Ergebnis von append immer der Variable zuweisen.

Slices teilen sich ein Underlying-Array — Mutationen sind sichtbar.

a := s[1:4] und b := s[2:5] zeigen auf denselben Speicher. Schreibst du a[1] = 99, sieht b und auch s den geänderten Wert. Wenn du echte Unabhängigkeit brauchst, kopiere mit copy oder slices.Clone.

nil-Slice vs. leerer Slice.

var s []int ist nil (s == nil ist true), []int{} ist nicht nil, aber hat Länge 0. Für len, cap, range und append verhalten sich beide identisch — der Unterschied wird nur sichtbar bei == nil-Vergleichen und bei JSON-Serialisierung (null vs. []).

len vs. cap und 3-Argument-Slicing s[lo:hi:max].

Bei s[lo:hi] erbt das neue Slice die Restkapazität bis zum Array-Ende. Wer eine API zurückgibt, ohne das zu beschneiden, lässt den Empfänger via append unbemerkt in fremde Speicherbereiche schreiben. s[lo:hi:max] deckelt die Kapazität — typisch in Builder-Mustern und beim Verkleinern von Slices.

Pointer auf die Loop-Variable einsammeln (vor Go 1.22).

In Versionen unter Go 1.22 teilten sich alle Iterationen einer for ... range-Schleife dieselbe Loop-Variable. append(out, &v) lieferte am Ende len-mal denselben Pointer auf das letzte Element. Seit Go 1.22 ist die Variable pro Iteration frisch — aber bestehender Code aus älteren Versionen hat den Bug oft noch.

Funktion modifiziert Slice — der Caller sieht das.

Übergibst du ein Slice an eine Funktion und änderst Elemente per Index, wirken die Änderungen direkt im Caller, weil der Slice-Header auf dasselbe Array zeigt. Was nicht zurück propagiert: Reslicing innerhalb der Funktion (s = s[1:]) und ein append, das reallokiert. Diese Änderungen brauchen einen Rückgabewert oder einen Pointer auf das Slice.

copy braucht ein Ziel mit Länge — nicht nur Kapazität.

copy(dst, src) kopiert min(len(dst), len(src)) Elemente. dst := make([]int, 0, 10) kopiert nichts, weil len(dst) == 0. Richtig ist dst := make([]int, len(src)) oder direkt slices.Clone(src).

range über Slice gibt Index plus Wert — der Wert ist eine Kopie.

for i, v := range s { v.Field = ... } ändert nichts am Slice, weil v eine Kopie ist. Mutieren willst du über s[i].Field = ... oder ein Slice von Pointern.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Datentypen

Zur Übersicht