Introduction
Dans la section précédente, nous avons abordé les tableaux en Go. Cependant, les tableaux présentent des limites : une fois déclarés et initialisés, leur longueur ne peut plus être modifiée. Par conséquent, les tableaux ne sont pas massivement utilisés dans la programmation quotidienne. En revanche, les slices (tranches) sont plus couramment utilisés et offrent une structure de données beaucoup plus flexible.
Points de connaissance :
- Définir un slice
- Initialiser un slice
- Opérations sur les slices : ajouter, supprimer, modifier et rechercher
- Extension d'un slice
- Troncature de slice
- Slices multidimensionnels
Qu'est-ce qu'un Slice
Les slices sont similaires aux tableaux ; ce sont des conteneurs qui stockent des éléments du même type de données. Cependant, les tableaux ont des limites : une fois déclarés et initialisés, leur longueur est fixe. Bien que les tableaux aient leurs cas d'utilisation, ils manquent de souplesse. C'est pourquoi les slices sont privilégiés dans le développement quotidien.
En Go, les slices sont implémentés à l'aide de tableaux. Un slice est essentiellement un tableau dynamique dont la taille peut varier. Nous pouvons effectuer des opérations telles que l'ajout, la suppression, la modification et la recherche d'éléments, ce qui est impossible avec un tableau classique de taille fixe.
Définir un Slice
La syntaxe d'initialisation d'un slice est très proche de celle d'un tableau. La différence majeure est qu'il n'est pas nécessaire de spécifier la longueur de l'élément. Examinons le code suivant :
// Déclarer un tableau d'une longueur de 5
var a1 [5]byte
// Déclarer un slice
var s1 []byte
Un Slice est une référence à un tableau
Si nous déclarons un tableau de type int, la valeur par défaut (zero value) de chaque élément sera 0.
Cependant, si nous déclarons un slice, sa valeur par défaut sera nil. Créons un fichier nommé slice.go pour vérifier cela :
touch ~/project/slice.go
Saisissez le code suivant :
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
Après l'exécution du code, la sortie suivante s'affichera :
true
true
Nous avons créé un tableau a et un slice s. Nous comparons le premier élément du tableau a avec zéro, et vérifions si le slice s est nil.
Comme nous pouvons le constater, lorsqu'on déclare un slice, sa valeur initiale est nil. C'est parce que les slices ne stockent pas directement les données ; ils ne font que référencer des tableaux. Le slice pointe vers une structure de tableau sous-jacente.
Structure de données d'un Slice
Un slice est un type de données composite, également appelé structure (struct). C'est un type composé de champs de différents types. La structure interne d'un slice se compose de trois éléments : un pointeur, une longueur (len) et une capacité (cap).
type slice struct {
elem *type
len int
cap int
}
Comme mentionné précédemment, la structure référence le tableau sous-jacent. Le pointeur elem pointe vers le premier élément du tableau, et type correspond au type des éléments du tableau référencé.
len et cap représentent respectivement la longueur et la capacité du slice. Vous pouvez utiliser les fonctions len() et cap() pour obtenir ces valeurs.
L'image suivante montre qu'un slice référence un tableau sous-jacent de type int, avec une longueur de 5 et une capacité de 5 :

Lorsque vous définissez un nouveau slice, le pointeur elem est initialisé à sa valeur nulle (soit nil). Le concept de pointeurs sera introduit dans les labs suivants. Pour l'instant, retenez qu'un pointeur désigne l'adresse mémoire d'une valeur. Dans l'image ci-dessus, le pointeur elem pointe vers l'adresse du premier élément du tableau sous-jacent.
Opérations sur les slices : Ajouter, Supprimer, Modifier et Rechercher
Troncature de tableaux ou de slices
Puisque la structure sous-jacente d'un slice est un tableau, nous pouvons extraire une portion spécifique du tableau pour servir de référence au slice. Le segment de code suivant illustre ce concept :
package main
import "fmt"
func main() {
// Définir un tableau d'entiers d'une longueur de 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Déclarer un slice vide
var s1 []int
fmt.Println("Le slice s1 est vide :", s1 == nil)
// Utiliser la troncature de tableau pour obtenir le slice
s1 = a[1:5]
s2 := a[2:5]
s3 := a[:]
fmt.Println("Les slices s1, s2 et s3 sont vides :", s1 == nil, s2 == nil, s3 == nil)
fmt.Println("Éléments du tableau a :", a)
fmt.Println("Éléments du slice s1 :", s1)
fmt.Println("Éléments du slice s2 :", s2)
fmt.Println("Éléments du slice s3 :", s3)
}
La sortie est la suivante :
Le slice s1 est vide : true
Les slices s1, s2 et s3 sont vides : false false false
Éléments du tableau a : [0 1 2 3 4 5 6 7 8 9]
Éléments du slice s1 : [1 2 3 4]
Éléments du slice s2 : [2 3 4]
Éléments du slice s3 : [0 1 2 3 4 5 6 7 8 9]
Dans ce programme, nous déclarons et initialisons d'abord le tableau a, puis nous utilisons la troncature pour assigner une partie du tableau au slice vide s1. Ce faisant, nous créons un nouveau slice.
s1[1:5] représente la création d'une tranche du tableau. La plage du slice va de l'indice 1 du tableau a jusqu'à l'indice 5, en excluant le cinquième élément.
Remarque : Dans les langages de programmation, le premier élément est à l'indice 0, pas 1. Ainsi, le deuxième élément du tableau a un indice de 1.
Nous utilisons l'opérateur := pour assigner directement le tableau tronqué au slice s2. Le même principe s'applique à s3, mais sans spécifier de plage, ce qui tronque tous les éléments du tableau.
L'image ci-dessous représente la troncature. Notez que la partie verte du slice représente une référence au tableau bleu. En d'autres termes, ils partagent tous le même tableau sous-jacent, qui est a.

La syntaxe de troncature pour les slices est la suivante :
[début:fin]
Les arguments début et fin sont optionnels. Si nous voulons obtenir tous les éléments du tableau, nous pouvons omettre les deux. C'est ce qui est démontré avec s3 := a[:].
Si nous voulons récupérer tous les éléments après un certain indice, nous pouvons omettre le paramètre fin. Par exemple, a1[3:] récupérera tous les éléments à partir de l'indice 3.
Pour récupérer tous les éléments avant un certain indice, nous pouvons omettre le paramètre début. Par exemple, a1[:4] extraira tous les éléments de l'indice 0 à l'indice 4, en excluant l'élément à l'indice 4.
En plus d'extraire un slice d'un tableau, nous pouvons également extraire un nouveau slice d'un slice existant. L'opération est identique. Voici un exemple simple :
package main
import "fmt"
func main() {
// Définir un tableau d'entiers d'une longueur de 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Créer le slice initial s1
var s1 []int
s1 = a[1:7]
fmt.Printf("Slice s1: %d\tLongueur: %d\tCapacité: %d\n", s1, len(s1), cap(s1))
// Extraire un nouveau slice s2 à partir du slice initial s1
s2 := s1[2:4]
fmt.Printf("Slice s2: %d\tLongueur: %d\tCapacité: %d\n", s2, len(s2), cap(s2))
}
La sortie est la suivante :
Slice s1: [1 2 3 4 5 6] Length: 6 Capacity: 9
Slice s2: [3 4] Length: 2 Capacity: 7
Dans ce programme, nous avons obtenu le slice s1 en tronquant le tableau a. La plage de s1 va de l'indice 1 à 7. Ensuite, nous avons extrait s2 de s1. Comme la troncature de s1 est contiguë, le nouveau slice s2 le sera également.
On remarque que la capacité d'un slice change lors de la troncature. Les règles sont les suivantes :
Si nous tronquons un slice ayant une capacité c, la longueur de s[i:j] sera j-i, et la capacité sera c-i.
Pour s1, le tableau sous-jacent est a, et la capacité de a[1:7] est 9 (soit 10-1).
Pour s2, le tableau sous-jacent est le même que celui de s1. Puisque la partie tronquée avait une capacité de 9 à l'étape précédente, la capacité de s1[2:4] devient 7 (soit 9-2).
Les modifications des valeurs d'un slice affectent simultanément le tableau sous-jacent
Puisque le slice ne stocke pas de données mais référence un tableau, modifier une valeur dans un slice changera simultanément la valeur dans le tableau d'origine. Démonstration :
package main
import "fmt"
func main() {
// Définir un tableau d'entiers d'une longueur de 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[2:5]
s2 := a[:]
fmt.Println("Avant modification : ")
fmt.Println("Éléments du tableau a : ", a)
fmt.Println("Éléments du slice s1 : ", s1)
fmt.Println("Éléments du slice s2 : ", s2)
// Modifier la valeur à l'indice 2 du slice s1 par 23
s1[2] = 23
fmt.Println("Après modification : ")
fmt.Println("Éléments du tableau a : ", a)
fmt.Println("Éléments du slice s1 : ", s1)
fmt.Println("Éléments du slice s2 : ", s2)
}
La sortie est la suivante :
Avant 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]
Après modification :
Elements in array a: [0 1 2 3 23 5 6 7 8 9]
Elements in array a: [2 3 23]
Elements in array a: [0 1 2 3 23 5 6 7 8 9]
Dans ce programme, les slices s1 et s2 référencent tous deux le tableau a. Lorsque s1 modifie sa valeur à l'indice 2 pour la passer à 23, les valeurs du tableau a et du slice s2 sont également mises à jour.
On constate que la valeur à l'indice 4 du tableau est modifiée en 23 (car s1 := a[2:5] signifie que s1[0] correspond à a[2], s1[1] à a[3], et s1[2] à a[4]). Cela entraîne la modification de la valeur à l'indice 4 tant pour le tableau a que pour le slice s2.
Ce comportement peut provoquer des bugs difficiles à déboguer. Par conséquent, dans la pratique, il convient d'éviter autant que possible que plusieurs slices référencent le même tableau sous-jacent de manière non contrôlée.
Ajouter des éléments à un slice
Dans cette section, nous introduisons la fonction append, utilisée pour ajouter des éléments à un slice. La syntaxe est la suivante :
func append(slice []Type, elems ...Type) []Type
Le premier argument est le slice cible, et les arguments suivants sont les éléments à ajouter. Le []Type à la fin indique que la fonction append retourne un nouveau slice du même type.
Les points de suspension ... après elems indiquent qu'il s'agit d'un paramètre variadique, ce qui signifie que vous pouvez passer un ou plusieurs éléments.
Voici un exemple d'utilisation de append :
package main
import "fmt"
func main() {
// Définir un tableau d'entiers d'une longueur de 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[1:7]
fmt.Printf("Valeur initiale s1 : %d\tLongueur : %d\tCapacité : %d\n", s1, len(s1), cap(s1))
s1 = append(s1, 12)
fmt.Printf("Valeur modifiée s1 : %d\tLongueur : %d\tCapacité : %d\n", s1, len(s1), cap(s1))
s1 = append(s1, 14, 14)
fmt.Printf("Valeur modifiée s1 : %d\tLongueur : %d\tCapacité : %d\n", s1, len(s1), cap(s1))
}
La sortie est la suivante :
Initial s1 value: [1 2 3 4 5 6] Length: 6 Capacity: 9
Modified s1 value: [1 2 3 4 5 6 12] Length: 7 Capacity: 9
Modified s1 value: [1 2 3 4 5 6 12 14 14] Length: 9 Capacity: 9
Dans ce programme, nous créons d'abord le slice s1 par troncature. Nous utilisons l'opérateur = pour réassigner la valeur de retour d'append à s1. Lorsqu'on ajoute un élément, la capacité peut changer. La capacité du slice sera généralement doublée si le nombre d'éléments dépasse la capacité actuelle.
Les règles d'extension des slices sont les suivantes :
- Si le tableau sous-jacent peut accueillir les nouveaux éléments, la capacité ne change pas.
- Si le tableau sous-jacent est trop petit, Go crée un nouveau tableau plus grand, y copie les valeurs existantes, puis ajoute les nouvelles valeurs.
Remarque : L'extension ne consiste pas toujours à doubler la taille ; cela dépend de la taille des éléments, du nombre d'éléments ajoutés et d'autres facteurs d'optimisation interne.
Supprimer des éléments dans un slice
Go ne fournit pas de mot-clé ou de fonction dédiée pour supprimer des éléments d'un slice, mais nous pouvons utiliser la troncature pour obtenir ce résultat.
Le code suivant supprime l'élément à l'indice 5 du slice s et l'assigne à un nouveau slice s1 :
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Afficher le slice `s` avant suppression
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Afficher les slices `s` et `s1` après suppression de l'élément à l'indice 5
fmt.Printf("%d\n%d\n", s, s1)
}
La sortie est la suivante :
[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 6 7 8 9 9]
[0 1 2 3 4 6 7 8 9]
La clé de la suppression réside dans l'instruction append. Elle ajoute les éléments situés après l'indice 6 aux éléments situés avant l'indice 5.
Cette opération équivaut à un écrasement. Par exemple, l'élément 5 est écrasé par l'élément 6, l'élément 6 par le 7, et ainsi de suite. Par conséquent, la valeur du tableau sous-jacent à ce stade devient [0 1 2 3 4 6 7 8 9 9].
Si nous vérifions la longueur et la capacité de s1, nous verrons que la longueur est de 9, mais la capacité reste de 10, car s1 est une référence à la troncature de s. Ils partagent le même tableau.
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:]...)
// Vérifier la longueur et la capacité de s1
fmt.Println(len(s1), cap(s1))
fmt.Printf("\n%d\n%d\n\n", s, s1)
// Modifier le slice s
s[3] = 22
fmt.Printf("%d\n%d\n", s, s1)
}
La sortie est la suivante :
[0 1 2 3 4 5 6 7 8 9]
9 10
[0 1 2 3 4 6 7 8 9 9]
[0 1 2 3 4 6 7 8 9]
[0 1 2 22 4 6 7 8 9 9]
[0 1 2 22 4 6 7 8 9]
Exercice
En plus de supprimer des éléments à des positions spécifiques, nous pouvons également supprimer une plage d'éléments. L'opération est similaire. Faisons un petit exercice.
Créez un fichier slice1.go. Créez un slice a et initialisez-le comme suit. Utilisez ensuite la troncature pour créer un autre slice s qui n'inclut pas les éléments dont la valeur est comprise entre 4 et 6 (inclus). Enfin, affichez le nouveau slice.
a := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
Sortie attendue :
[9 8 7 3 2 1 0]
- Indice : Faites attention aux indices du slice (n'oubliez pas que l'indice commence à 0).
- Exigences : Le fichier
slice1.godoit être placé dans le répertoire~/project.
Extension des Slices
Un slice possède deux attributs : len (longueur) et cap (capacité). len représente le nombre d'éléments actuellement présents, tandis que cap représente le nombre maximum d'éléments que le slice peut contenir sans réallocation.
Que se passe-t-il lorsque le nombre d'éléments ajoutés dépasse la capacité ? Découvrons-le :
package main
import "fmt"
func main() {
s1 := make([]int, 3)
s2 := make([]int, 3, 5)
fmt.Println("Avant Append dans s1 :", s1, "Longueur :", len(s1), "Capacité :", cap(s1))
fmt.Println("Avant Append dans s2 :", s2, "Longueur :", len(s2), "Capacité :", cap(s2))
s1 = append(s1, 12)
s2 = append(s2, 22)
fmt.Println("Après Append dans s1 :", s1, "Longueur :", len(s1), "Capacité :", cap(s1))
fmt.Println("Après Append dans s2 :", s2, "Longueur :", len(s2), "Capacité :", cap(s2))
}
Pour exécuter le programme, utilisez la commande suivante :
go run slice.go
La sortie est la suivante :
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
Comme nous le voyons, lorsque nous ajoutons des éléments et que la taille dépasse la capacité d'origine, la capacité du slice est automatiquement augmentée.
Les règles d'extension sont les suivantes :
- Si le tableau sous-jacent peut contenir les nouveaux éléments, la capacité ne change pas.
- Sinon, Go crée un nouveau tableau plus grand, copie les anciennes valeurs et ajoute les nouvelles.
Remarque : Le doublement de la capacité n'est pas systématique ; cela dépend de la taille des données et de l'implémentation interne de Go.
Copier des Slices
Nous pouvons utiliser la fonction copy pour dupliquer un slice vers un autre. La syntaxe est la suivante :
func copy(dst, src []Type) int
dst est le slice de destination, src est le slice source, et l'entier retourné indique le nombre d'éléments copiés, qui correspond au minimum entre len(dst) et len(src).
Remarque : La fonction copy n'ajoute pas d'éléments, elle remplace les éléments existants dans la destination.
Voici un exemple :
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3}
s2 := []int{8, 9}
s3 := []int{0, 1, 2, 3}
s4 := []int{8, 9}
// Copier s1 vers s2
n1 := copy(s2, s1)
// Copier s4 vers s3
n2 := copy(s3, s4)
fmt.Println(n1, s1, s2)
fmt.Println(n2, s3, s4)
}
Pour exécuter le programme :
go run slice.go
La sortie est la suivante :
2 [0 1 2 3] [0 1]
2 [8 9 2 3] [8 9]
Dans ce programme, nous avons copié les valeurs de s1 vers s2, et de s4 vers s3. La fonction copy renvoie le nombre d'éléments effectivement copiés.
On remarque que la première fonction copy copie s1[0, 1, 2, 3] vers s2[8, 9]. Comme la longueur minimale entre les deux est 2, seuls 2 éléments sont copiés. Le slice de destination s2 devient [0, 1].
La seconde copie s4[8, 9] vers s3[0, 1, 2, 3]. Là encore, 2 éléments sont copiés. En conséquence, s3 devient [8, 9, 2, 3].
Parcourir les Slices
Le parcours d'un slice est identique à celui d'un tableau. Toutes les méthodes de parcours de tableaux s'appliquent également aux slices.
Exercice
Dans cet exercice, nous allons tester et renforcer notre compréhension du parcours des slices et des tableaux.
Créez un fichier slice2.go. Déclarez un tableau a1 et un slice s1, et initialisez-les comme suit. Ensuite, itérez sur les éléments du tableau a1 et du slice s1, et affichez leurs indices et leurs valeurs.
a1 := [5]int{1, 2, 3, 9, 7}
s1 := []int{1, 8, 12, 1, 3}
Sortie attendue :
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
- Exigences : Le fichier
slice2.godoit être placé dans le répertoire~/project. - Indice : Vous pouvez utiliser la structure
rangeou le format d'indexation classique pour itérer à travers les éléments.
Résumé
Dans cette section, nous avons exploré les slices et leur utilisation. Comparativement, les slices sont plus flexibles et polyvalents que les tableaux. Lors de la manipulation de plusieurs slices, il convient d'être prudent pour éviter des effets de bord inattendus dus au partage du tableau sous-jacent.



