navigation Navigation


Inhaltsverzeichnis

Slice


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.

Inhaltsverzeichnis

    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.

    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))
    }
    Array: [10 20 30 0 0]
    Länge: 

    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.

    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)
    }
    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.

    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)
    
    }
    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 usprü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.
    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))
    }
    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.

    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))
        }
    }
    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.

    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]))
        }
    }
    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 ideals, wenn man bereits weiß, welche Werte das Slice enthalten soll.

    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)
    }
    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.

    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)
    }
    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.

    info

    Man kann nur auf die Elemente eines Slices zugreifen, welche als Länge angegeben wurden. Diese werden mit Null-Werten initialisiert.

    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))
    
    }
    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.

    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)
    }
    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.

    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))
    }
    Slice: []
    Länge: 0
    Kapazität: 0
    Slice aktualisiert: [1 2 3]
    Länge aktualisiert: 3

    Slice Operationen

    Slices unterstützen einige von 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.

    Beispiel - append
    package main
    
    import "fmt"
    
    func main() {
        s := []int{1, 2, 3}
        fmt.Printf("Original: %v\n", s)
    
        // Ein Elemente 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)
    }
    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.

    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))
    }
    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 kompletes Slice an ein anderes Slice anhängen, indem man den Spread-Operator ... verwendet.

    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)
    }
    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.

    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) 
    }
    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.

    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)
    }
    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.

    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)
    }
    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.

    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)
    }
    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.

    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)
    }
    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.

    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)
    }
    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.

    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)
    }
    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.

    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)
    }
    --- 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.

    Normales Slicing
    s[low:high]
    
    Länge = high - low
    Kapazität = cap(s) - low

    Beim oberen Beispiel würde es bedeuten:

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

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

    Syntax
    s[low:high:max]

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

    Go berechnet dann:

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

    Das bedeutet dann in Bezug auf das obere Beispiel Folgendes:

    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.

    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()
    }
    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.

    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()
    }
    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.

    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))
    }
    ``
    </div>
    <div slot="output">

    Slice s[2:5]: [2 3 4] Länge slice: 3 Kapazität slice: 8

    </div>
    </CodeBlock>
    
    Das Kapazitätswachtum 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.
    
    <CodeBlock filename="Beispiel - Kapazität Wachstum" showOutput={true}>
    <div slot="code">
    ```go
    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)
            }
        }
    }
    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.

    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))
    }