Une slice en un exemple
Une slice est une vue extensible sur un tableau. Vous en créez une avec un littéral ou avec make, l'agrandissez avec append, et lisez sa taille avec len.
[]string n'a pas de longueur entre les crochets. C'est la différence avec un type tableau comme [4]string, dont la taille est fixe.
Ce qu'est une slice
Une valeur slice est un petit en-tête à trois champs :
- un pointeur vers un élément d'un tableau sous-jacent
- une longueur : le nombre d'éléments que la slice peut voir
- une capacité : le nombre d'éléments qui existent entre ce pointeur et la fin du tableau sous-jacent
Copier une slice (l'affecter, la passer à une fonction) ne copie que cet en-tête. Les deux copies pointent vers les mêmes éléments. Presque toutes les surprises avec les slices viennent de cet oubli.
make : longueur et capacité
make([]T, len, cap) alloue un tableau sous-jacent et renvoie une slice sur celui-ci. La capacité est facultative et vaut par défaut la longueur.
Une confusion courante : make([]int, 5) suivi de cinq appels à append vous donne dix éléments, dont les cinq premiers à zéro. Utilisez make([]int, 0, 5) quand vous comptez faire des append, ou make([]int, 5) quand vous comptez affecter par indice.
Préallouer la capacité quand vous connaissez la taille finale évite des agrandissements répétés. C'est une optimisation, pas une obligation.
append et la croissance
append ajoute des éléments après la longueur actuelle. Si la capacité suffit, il écrit sur place. Sinon, il alloue un tableau plus grand, y copie les anciens éléments, et renvoie une slice qui pointe vers le nouveau tableau.
La capacité double à peu près tant que la slice est petite et augmente d'un facteur plus faible (qui tend vers 1,25) une fois passés 256 éléments. Les chiffres exacts sont un détail du runtime qui a changé d'une version de Go à l'autre, donc n'écrivez jamais de code qui en dépend.
Comme append peut renvoyer un tableau différent, vous devez utiliser son résultat :
append(s, 4) // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct
Ajouter une slice entière utilise la syntaxe d'expansion : s = append(s, other...).
Le découpage partage le tableau sous-jacent
s[low:high] crée un nouvel en-tête de slice sur le même tableau, couvrant les indices de low jusqu'à high exclu. L'une ou l'autre borne peut être omise. Aucun élément n'est copié.
Sortie :
[10 99 30 40 50]
3 4
Ce partage est ce qui rend le découpage peu coûteux. C'est aussi la source du bug de slice le plus célèbre.
Le bug d'aliasing de append
Quand une sous-slice a de la capacité libre, un append sur elle écrit dans le tableau du parent, par-dessus des éléments que le parent utilise encore.
Sortie :
[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]
a et b partagent la case du tableau qui suit "cmd", donc le second append a écrasé le premier. Le code a l'air correct et fonctionne chaque fois que la capacité se trouve être pleine, c'est pourquoi ce bug n'apparaît que de temps en temps.
Deux corrections :
- L'expression de slice complète
s[low:high:max]plafonne la capacité àmax-low. Sans capacité libre, le prochainappenddoit allouer.first := base[:2:2]rend le premier exemple sûr. - Copiez explicitement quand une slice va survivre à l'appel ou recevoir des append indépendamment :
slices.Clone(prefix)ouappend([]string(nil), prefix...).
Un piège lié : une petite slice d'un énorme tableau garde tout le tableau en vie pour le ramasse-miettes. Si vous lisez un fichier de 100 Mo et gardez data[:10], les 100 Mo restent en mémoire. Clonez la partie que vous gardez.
copy
La fonction intégrée copy(dst, src) copie min(len(dst), len(src)) éléments et renvoie ce nombre. Elle n'agrandit jamais dst.
copy gère correctement une source et une destination qui se chevauchent, donc copy(s[1:], s) décale les éléments vers la droite sans corruption.
Le package slices
Depuis Go 1.21, le package standard slices propose des fonctions génériques pour les opérations que vous écriviez autrefois à la main.
| Fonction | Ce qu'elle fait |
|---|---|
Contains, Index | trouver une valeur (ContainsFunc, IndexFunc prennent un prédicat) |
Sort, SortFunc, SortStableFunc | trier sur place |
BinarySearch | chercher dans une slice triée |
Insert, Delete, DeleteFunc | insérer ou retirer, renvoient la nouvelle slice |
Compact | retirer les doublons consécutifs |
Equal, Compare | comparer élément par élément |
Clone, Reverse, Max, Min | ce que leur nom indique |
Collect, Sorted, Values, All | travailler avec des itérateurs (Go 1.23) |
Delete et Insert renvoient une slice que vous devez affecter, comme append. Le tri a sa propre page : le tri.
Retirer des éléments en bouclant par indice fait sauter des éléments. Utilisez plutôt slices.DeleteFunc :
s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens
Slices 2D
Une slice de slices vous donne une grille dont les lignes peuvent avoir des longueurs différentes. Chaque ligne doit être allouée séparément.
Oublier le make intérieur laisse chaque ligne à nil, et grid[1][2] = 7 provoque un panic index out of range [2] with length 0.
Slices nil et slices vides
Une slice nil se comporte comme une slice vide pour len, cap, range et append, donc préférez var s []T comme état zéro. N'initialisez à []T{} que lorsque la différence compte, c'est-à-dire surtout pour la sortie JSON (null ou []). Testez la vacuité avec len(s) == 0, pas s == nil.
Slices et fonctions
Une fonction qui reçoit une slice peut modifier ses éléments, et l'appelant voit les modifications. Elle ne peut pas changer la longueur de l'appelant, parce qu'elle a reçu une copie de l'en-tête. Une fonction qui fait des append doit renvoyer la nouvelle slice :
func addAll(s []int, vals ...int) []int {
return append(s, vals...)
}
C'est pourquoi append, slices.Delete et slices.Insert renvoient tous une slice.
Erreurs courantes
- Ignorer le résultat de
append. Ne compile pas quand il est totalement jeté, maisappend(s, x)affecté à une autre variable que celle que vous continuez d'utiliser est un bug de logique. - Deux append à partir d'une même base. Ils peuvent partager le stockage. Clonez la base ou utilisez une expression de slice complète.
make([]T, n)puisappend. Laisse n zéros au début.- Indice hors limites.
s[len(s)]provoque un panic. Le dernier élément ests[len(s)-1]. - Comparer des slices avec
==. Seuls == nilcompile. Utilisezslices.Equal.
Questions fréquentes
Quelle est la différence entre la longueur et la capacité d'une slice en Go ?
len(s) est le nombre d'éléments que la slice contient actuellement. cap(s) est le nombre d'éléments qui tiennent dans le tableau sous-jacent à partir du premier élément de la slice. append écrit dans la capacité libre quand il y en a, et alloue un nouveau tableau plus grand quand il n'y en a pas.
Comment ajouter un élément à une slice en Go ?
Appelez la fonction intégrée append et réaffectez le résultat : s = append(s, x). Elle peut ajouter plusieurs valeurs (append(s, 1, 2, 3)) ou une autre slice (append(s, other...)). Utilisez toujours la valeur de retour, car append peut renvoyer une slice qui pointe vers un nouveau tableau.
Comment supprimer un élément d'une slice en Go ?
Utilisez slices.Delete(s, i, i+1) (Go 1.21), qui décale les éléments suivants et renvoie la slice raccourcie. Depuis Go 1.22, elle met aussi à zéro les cases libérées en fin de tableau. Si l'ordre n'a pas d'importance, échangez avec le dernier élément et tronquez : s[i] = s[len(s)-1]; s = s[:len(s)-1].
Comment vérifier si une slice contient une valeur en Go ?
Utilisez slices.Contains(s, v) du package standard slices (Go 1.21). slices.Index(s, v) renvoie la position ou -1, et slices.ContainsFunc prend un prédicat. Pour des recherches répétées sur de gros volumes, construisez plutôt une map.
Quelle est la différence entre une slice nil et une slice vide en Go ?
var s []int est nil ; s := []int{} est vide mais pas nil. Les deux ont une longueur de 0, et les deux fonctionnent avec len, range et append. La différence apparaît dans les tests s == nil et dans encoding/json, qui encode une slice nil en null et une slice vide en [].