Im vorherigen Abschnitt haben wir Arrays in Go besprochen. Allerdings haben Arrays Einschränkungen: Sobald sie deklariert und initialisiert sind, kann ihre Länge nicht geändert werden. Daher werden Arrays in der täglichen Programmierung nicht weit verbreitet eingesetzt. Im Gegensatz dazu werden Slices (Schnitte) häufiger verwendet und bieten eine flexiblere Datenstruktur.
Wissenspunkte:
Definition eines Slices
Initialisierung eines Slices
Operationen auf Slices, d. h. Hinzufügen, Löschen, Ändern und Suchen
Erweiterung eines Slices
Kürzung eines Slices
Mehrdimensionale Slices
Dies ist ein Guided Lab, das schrittweise Anweisungen bietet, um Ihnen beim Lernen und Üben zu helfen. Befolgen Sie die Anweisungen sorgfältig, um jeden Schritt abzuschließen und praktische Erfahrungen zu sammeln. Historische Daten zeigen, dass dies ein Labor der Stufe Anfänger mit einer Abschlussquote von 100% ist. Es hat eine positive Bewertungsrate von 100% von den Lernenden erhalten.
Praxis statt nur Theorie.
Was ist ein Slice?
Slices (Schnitte) sind ähnlich wie Arrays; sie sind Container, die Elemente desselben Datentyps enthalten. Allerdings haben Arrays Einschränkungen: Sobald sie deklariert und initialisiert sind, kann ihre Länge nicht geändert werden. Obwohl Arrays ihre Anwendungsfälle haben, sind sie nicht so flexibel. Daher werden Slices in der täglichen Programmierung häufiger verwendet.
In Go werden Slices mithilfe von Arrays implementiert. Ein Slice ist im Wesentlichen ein dynamisches Array, dessen Länge sich ändern kann. Wir können Operationen wie das Hinzufügen, Löschen, Ändern und Suchen von Elementen in einem Slice durchführen, was mit einem Array nicht möglich ist.
Definition eines Slices
Die Initialisierungssyntax für ein Slice ist der eines Arrays sehr ähnlich. Der Hauptunterschied besteht darin, dass die Länge der Elemente nicht angegeben werden muss. Schauen wir uns den folgenden Code an:
// Declare an array with a length of 5
var a1 [5]byte
// Declare a slice
var s1 []byte
Mehr als nur ein Lerntext – erlebe es selbst.
Ein Slice ist eine Referenz auf ein Array
Wenn wir ein Array vom Typ int deklarieren, ist der Nullwert für jedes Element des Arrays 0.
Allerdings ist der Nullwert für ein Slice, wenn wir es deklarieren, nil. Lassen Sie uns eine Datei namens slice.go erstellen, um dies zu überprüfen:
touch ~/project/slice.go
Geben Sie den folgenden Code ein:
package main
import "fmt"
func main() {
var a [3]int
var s []int
fmt.Println(a[0] == 0) // true
fmt.Println(s == nil) // true
}
go run slice.go
Nachdem Sie den Code ausgeführt haben, wird die folgende Ausgabe angezeigt:
true
true
Wir haben ein Array a und ein Slice s erstellt. Wir vergleichen das erste Element des Arrays a mit Null und prüfen, ob das Slice snil ist.
Wie wir sehen können, ist der Nullwert eines Slices nil, wenn wir es deklarieren. Dies liegt daran, dass Slices keine Daten speichern; sie verweisen nur auf Arrays. Das Slice zeigt auf die zugrunde liegende Array-Struktur.
Datenstruktur eines Slices
Ein Slice ist ein zusammengesetzter Datentyp, auch als Struktur (struct) bekannt. Es ist ein zusammengesetzter Typ, der aus Feldern unterschiedlicher Typen besteht. Die interne Struktur eines Slices besteht aus drei Elementen: einem Zeiger, einer Länge und einer Kapazität.
type slice struct {
elem *type
len int
cap int
}
Wie bereits erwähnt, verweist die Struktur auf das zugrunde liegende Array. Der elem-Zeiger zeigt auf das erste Element des Arrays, und type ist der Typ des referenzierten Array-Elements.
len und cap stellen die Länge und die Kapazität des Slices dar. Sie können die Funktionen len() und cap() verwenden, um die Länge und die Kapazität des Slices zu erhalten.
Das folgende Bild zeigt, dass das Slice auf ein zugrunde liegendes Array vom Typ int verweist und eine Länge von 8 und eine Kapazität von 10 hat:
Wenn Sie ein neues Slice definieren, wird der elem-Zeiger auf den Nullwert (d. h. nil) initialisiert. Das Konzept von Zeigern wird in späteren Labs eingeführt. Beachten Sie vorerst, dass ein Zeiger auf die Speicheradresse eines Werts zeigt. Im obigen Bild zeigt der elem-Zeiger auf die Adresse des ersten Elements des zugrunde liegenden Arrays.
Direkt aus dem Tutorial heraus programmieren.
Operationen auf Slices: Hinzufügen, Löschen, Ändern und Suchen
Kürzen von Arrays oder Slices
Da die zugrunde liegende Struktur eines Slices ein Array ist, können wir eine bestimmte Länge des Arrays als Referenz für das Slice extrahieren. Das folgende Code-Segment zeigt dies:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Declare an empty slice
var s1 []int
fmt.Println("Slice s1 is empty:", s1 == nil)
// Use array truncation to obtain the slice
s1 = a[1:5]
s2 := a[2:5]
s3 := a[:]
fmt.Println("Slice s1, s2, and s3 are empty:", s1 == nil, s2 == nil, s3 == nil)
fmt.Println("Elements in array a:", a)
fmt.Println("Elements in slice s1:", s1)
fmt.Println("Elements in slice s2:", s2)
fmt.Println("Elements in slice s3:", s3)
}
Die Ausgabe ist wie folgt:
Slice s1 is empty: true
Slice s1, s2, and s3 are empty: false false false
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in slice s1: [1 2 3 4]
Elements in slice s2: [2 3 4]
Elements in slice s3: [0 1 2 3 4 5 6 7 8 9]
In diesem Programm deklarieren und initialisieren wir zunächst das Array a und verwenden dann die Array-Kürzung, um einen Teil des Arrays dem leeren Slice s1 zuzuweisen. Dadurch erstellen wir ein neues Slice.
s1[1:5] repräsentiert die Erstellung eines Slices aus dem Array. Der Bereich des Slices geht von Index 1 des Arrays a bis Index 5, wobei das fünfte Element ausgeschlossen ist.
Hinweis: In Programmiersprachen beginnt der Index des ersten Elements mit 0, nicht mit 1. Entsprechend hat das zweite Element im Array den Index 1.
Wir verwenden den :=-Operator, um das gekürzte Array direkt dem Slice s2 zuzuweisen. Das Gleiche gilt für s3, aber hier wird kein Bereich angegeben, sodass alle Elemente des Arrays gekürzt werden.
Das folgende Bild zeigt die Kürzung. Beachten Sie, dass der grüne Teil des Slices auf das blaue Array verweist. Mit anderen Worten, sie teilen sich dasselbe zugrunde liegende Array, nämlich a.
Die Syntax für die Kürzung von Slices lautet wie folgt:
[start:end]
Sowohl start als auch end sind optionale Argumente. Wenn wir alle Elemente des Arrays erhalten möchten, können wir sowohl das start- als auch das end-Argument weglassen. Dies wird in s3 := a[:] im vorherigen Programm gezeigt.
Wenn wir alle Elemente nach einem bestimmten Index abrufen möchten, können wir das end-Argument weglassen. Beispielsweise wird a1[3:] alle Elemente ab Index 3 abrufen.
Um alle Elemente vor einem bestimmten Index abzurufen, können wir das start-Argument weglassen. Beispielsweise wird a1[:4] alle Elemente von Index 0 bis Index 4 extrahieren, wobei das Element an Index 4 ausgeschlossen ist.
Neben der Extraktion eines Slices aus einem Array können wir auch ein neues Slice aus einem bestehenden Slice extrahieren. Die Operation ist dieselbe wie bei Arrays. Das folgende ist ein einfaches Beispiel:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Create the initial slice s1
var s1 []int
s1 = a[1:7]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s1, len(s1), cap(s1))
// Extract a new slice s2 from the initial slice s1
s2 := s1[2:4]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s2, len(s2), cap(s2))
}
In diesem Programm haben wir das Slice s1 durch Kürzung des Arrays a erhalten. Der Bereich von s1 geht von Index 1 bis Index 7. Mit := haben wir ein neues Slice s2 aus s1 extrahiert. Da die Kürzung von s1 zusammenhängend ist, wird auch das neue Slice s2 zusammenhängend sein.
Wir bemerken, dass die Kapazität eines Slices sich ändert, wenn wir es kürzen. Die Regeln lauten wie folgt:
Wenn wir ein Slice mit einer Kapazität von c kürzen, beträgt die Länge von s[i:j]j - i, und die Kapazität beträgt c - i.
Für s1 ist das zugrunde liegende Array a, und die Kapazität von a[1:7] beträgt 9 (d. h. 10 - 1).
Für s2 ist das zugrunde liegende Array dasselbe wie für s1. Da der gekürzte Teil in der vorherigen Stufe eine Kapazität von 9 hat, beträgt die Kapazität von s1[2:4] 7 (d. h. 9 - 2).
Änderungen an Slice-Werten beeinflussen gleichzeitig die Werte der zugrunde liegenden Array-Elemente
Da das Slice keine Daten speichert, sondern nur auf ein Array verweist, wird die Änderung des Werts eines Slices auch gleichzeitig den Wert des zugrunde liegenden Arrays ändern. Lassen Sie uns dies demonstrieren:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[2:5]
s2 := a[:]
fmt.Println("Before Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
// Modify the value at index 2 of the slice s1 to 23
s1[2] = 23
fmt.Println("After Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
}
Die Ausgabe ist wie folgt:
Before Modification:
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in array a: [2 3 4]
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
After Modification:
Elements in array a: [0 1 23 3 4 5 6 7 8 9]
Elements in array a: [2 3 23]
Elements in array a: [0 1 23 3 4 5 6 7 8 9]
In diesem Programm verweisen sowohl das Slice s1 als auch s2 auf das Array a. Wenn das Slice s1 den Wert an Index 2 auf 23 ändert, werden auch die Werte des Arrays a und des Slices s2 aktualisiert.
Wir können sehen, dass der Wert an Index 2 des Arrays auf 23 geändert wird, was dazu führt, dass der Wert an Index 4 des Slices s2 geändert wird.
Dies kann in der Programmentwicklung schwer zu debuggende Fehler verursachen. Daher sollten wir in der täglichen Programmierung versuchen, es so weit wie möglich zu vermeiden, dass mehrere Slices auf dasselbe zugrunde liegende Array verweisen.
Anhängen von Elementen an ein Slice
In diesem Abschnitt werden wir die append-Funktion einführen, die zum Hinzufügen von Elementen zu einem Slice verwendet wird. Die Syntax lautet wie folgt:
func append(slice []Type, elems...Type) []Type
Das erste Argument ist das Slice slice, und die verbleibenden Argumente sind die Elemente, die dem Slice hinzugefügt werden sollen. Das []Type am Ende gibt an, dass die append-Funktion ein neues Slice mit demselben Datentyp wie slice zurückgibt.
Das elems nach ... bedeutet, dass es sich um einen variablen Parameter handelt, was bedeutet, dass ein oder mehrere Parameter eingegeben werden können.
Hier ist ein Beispiel für die Verwendung der append-Funktion:
In diesem Programm erstellen wir zunächst das Slice s1 durch Kürzung des Arrays a. Der Bereich von s1 geht von Index 1 bis Index 7. Wir verwenden den :=-Operator, um den Rückgabewert von append an s1 zuzuweisen. Wenn wir ein Element mit append hinzufügen, wird sich die Kapazität des Slices ändern. Die Kapazität von s1 wird verdoppelt, wenn die Anzahl der Elemente die Kapazität überschreitet.
Die Regeln für die Erweiterung von Slices lauten wie folgt:
Wenn das zugrunde liegende Array eines Slices neue Elemente aufnehmen kann, ändert sich die Kapazität des Slices nicht.
Wenn das zugrunde liegende Array eines Slices keine neuen Elemente aufnehmen kann, erstellt Go ein größeres Array, kopiert die Werte des ursprünglichen Slices in das neue Array und fügt dann den angehängten Wert dem neuen Array hinzu.
Hinweis: Die Erweiterung von Slices ist nicht immer eine Verdopplung; es hängt von der Größe der Elemente, der Anzahl der erweiterten Elemente und der Hardware des Computers u. a. Faktoren ab. Weitere Informationen finden Sie im fortgeschrittenen Abschnitt über Slices.
Löschen von Elementen in einem Slice
Go bietet keine Schlüsselwörter oder Funktionen zum Löschen von Elementen aus einem Slice, aber wir können die Array-Kürzung verwenden, um dieselbe Funktionalität oder eine noch leistungsfähigere Fähigkeit zu erreichen.
Der folgende Code löscht das Element an Index 5 im Slice s und weist es einem neuen Slice s1 zu:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Print the slice `s` before deleting
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Print the slices `s` and `s1` after deleting the element at index 5
fmt.Printf("%d\n%d\n", s, s1)
}
Der Schlüssel zum Löschen eines bestimmten Index liegt in der append-Anweisung. Sie hängt die Elemente nach Index 6 an die Elemente vor Index 5 an.
Diese Operation entspricht einer Vorüberschreibung. Beispielsweise wird Element 5 mit Element 6 überschrieben, Element 6 mit Element 7 usw., bis Element 9, das dem neuen Slice hinzugefügt wird. Daher ist der Wert des zugrunde liegenden Arrays an diesem Punkt [0 1 2 3 4 6 7 8 9 9].
An diesem Punkt können wir, wenn wir die Länge und die Kapazität von s1 überprüfen, sehen, dass die Länge 9 beträgt, aber die Kapazität 10. Dies liegt daran, dass s1 eine Referenz auf die Kürzung von s ist. Sie teilen sich dasselbe zugrunde liegende Array.
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Check the length and capacity of s1
fmt.Println(len(s1), cap(s1))
fmt.Printf("\n%d\n%d\n\n", s, s1)
// Modify the slice s
s[3] = 22
fmt.Printf("%d\n%d\n", s, s1)
}
Neben dem Löschen von Elementen an bestimmten Positionen können wir auch einen bestimmten Bereich von Elementen aus einem Slice mithilfe der Kürzung löschen. Die Operation ist dieselbe wie beim Löschen eines Elements an einem bestimmten Index. Lassen Sie uns eine kleine Übung machen.
Erstellen Sie eine Datei slice1.go. Erstellen Sie ein Slice a und initialisieren Sie es wie folgt. Verwenden Sie dann die Kürzung, um ein weiteres Slice s zu erstellen, das keine Elemente größer als 3 oder kleiner als 7 enthält. Geben Sie schließlich das neue Slice aus.
a := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
Ausgabe:
[9 8 7 3 2 1 0]
Hinweis: Achten Sie auf den Startindex des Slices (denken Sie daran, dass der Index bei 0 beginnt).
Anforderungen: Die Datei slice1.go muss im Verzeichnis ~/project platziert werden.
Ein Slice hat zwei Attribute: len und cap. len repräsentiert die Anzahl der aktuell im Slice enthaltenen Elemente, während cap die maximale Anzahl der Elemente angibt, die das Slice aufnehmen kann.
Was passiert, wenn die Anzahl der dem Slice hinzugefügten Elemente seine Kapazität überschreitet? Lassen Sie uns es gemeinsam herausfinden:
Um das Programm auszuführen, führen Sie den folgenden Befehl aus:
go run slice.go
Die Ausgabe ist wie folgt:
Before Append in s1: [0 0 0] Length: 3 Capacity: 3
Before Append in s2: [0 0 0] Length: 3 Capacity: 5
After Append in s1: [0 0 0 12] Length: 4 Capacity: 6
After Append in s2: [0 0 0 22] Length: 4 Capacity: 5
Wie wir in diesem Programm sehen können, wird die Kapazität eines Slices automatisch erhöht, wenn wir Elemente hinzufügen und die Anzahl der Elemente die ursprüngliche Kapazität überschreitet.
Die Regeln für die Erweiterung eines Slices lauten wie folgt:
Wenn das zugrunde liegende Array eines Slices neue Elemente aufnehmen kann, ändert sich die Kapazität des Slices nicht.
Wenn das zugrunde liegende Array eines Slices keine neuen Elemente aufnehmen kann, erstellt Go ein größeres Array, kopiert die Werte aus dem ursprünglichen Slice in das neue Array und fügt dann die angehängten Werte dem neuen Array hinzu.
Hinweis: Die Kapazität eines Slices wird nicht immer verdoppelt; es hängt von der Größe der Elemente, der Anzahl der Elemente und der Hardware des Computers ab. Weitere Informationen finden Sie im fortgeschrittenen Abschnitt über Slices.
Kopieren von Slices
Wir können die copy-Funktion verwenden, um ein Slice in ein anderes zu kopieren. Die Syntax lautet wie folgt:
func copy(dst, src []Type) int
dst ist das Ziel-Slice, src ist das Quell-Slice, und das abschließende int gibt die Anzahl der kopierten Elemente an, die das Minimum von len(dst) und len(src) ist.
Hinweis: Die copy-Funktion fügt keine Elemente hinzu.
Um das Programm auszuführen, führen Sie den folgenden Befehl aus:
go run slice.go
Die Ausgabe ist wie folgt:
2 [0 1 2 3] [0 1]
2 [8 9 2 3] [8 9]
In diesem Programm haben wir die Werte des Slices s1 in das Slice s2 und die Werte des Slices s4 in das Slice s3 kopiert. Die copy-Funktion gibt die Anzahl der kopierten Elemente zurück.
Wir bemerken, dass die Werte von s1 und s2 sowie die Werte von s3 und s4 übereinstimmen. Die erste copy-Funktion kopiert s1[0, 1, 2, 3] in s2[8, 9]. Da die minimale Länge von s1 und s2 2 ist, werden 2 Werte kopiert. Das Ziel-Slice s2 wird zu [0, 1] geändert.
Die zweite copy-Funktion kopiert s4[8, 9] in s3[0, 1, 2, 3]. Da die minimale Länge von s3 und s4 2 ist, werden 2 Werte kopiert. Dadurch wird s3 zu [8, 9, 2, 3] geändert.
Durchlaufen von Slices
Das Durchlaufen eines Slices ähnelt dem Durchlaufen eines Arrays. Alle Methoden zum Durchlaufen von Arrays können auch für Slices verwendet werden.
Übung
In dieser Übung werden wir unser Verständnis des Durchlaufens von Slices und Arrays testen und festigen.
Erstellen Sie eine Datei slice2.go. Deklarieren Sie ein Array a1 und ein Slice s1 und initialisieren Sie sie wie folgt. Iterieren Sie dann über die Elemente im Array a1 und im Slice s1 und geben Sie ihre Indizes und Werte aus.
Element a1 at index 0 is 1
Element a1 at index 1 is 2
Element a1 at index 2 is 3
Element a1 at index 3 is 9
Element a1 at index 4 is 7
Element s1 at index 0 is 1
Element s1 at index 1 is 8
Element s1 at index 2 is 12
Element s1 at index 3 is 1
Element s1 at index 4 is 3
Anforderungen: Die Datei slice2.go muss im Verzeichnis ~/project platziert werden.
Hinweis: Sie können das 'range'-Format oder das Index-Format verwenden, um durch die Elemente zu iterieren.
In diesem Abschnitt haben wir uns mit Slices und ihrer Verwendung beschäftigt. Im Vergleich zu Arrays sind Slices flexibler und vielseitiger. Wenn wir mit mehreren Slices arbeiten, müssen wir vorsichtig sein, um unerwartete Slice-Operationen zu vermeiden.