Menu

Slices en Golang : append, make, len et cap, et le bug du partage

Les slices sont le type liste du quotidien en Go. Découvrez make, append et la croissance, len et cap, comment le découpage partage le tableau sous-jacent (et le bug que cela provoque), copy, le package slices, les slices 2D, et nil ou vide.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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 :

  1. L'expression de slice complète s[low:high:max] plafonne la capacité à max-low. Sans capacité libre, le prochain append doit allouer. first := base[:2:2] rend le premier exemple sûr.
  2. Copiez explicitement quand une slice va survivre à l'appel ou recevoir des append indépendamment : slices.Clone(prefix) ou append([]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.

FonctionCe qu'elle fait
Contains, Indextrouver une valeur (ContainsFunc, IndexFunc prennent un prédicat)
Sort, SortFunc, SortStableFunctrier sur place
BinarySearchchercher dans une slice triée
Insert, Delete, DeleteFuncinsérer ou retirer, renvoient la nouvelle slice
Compactretirer les doublons consécutifs
Equal, Comparecomparer élément par élément
Clone, Reverse, Max, Mince que leur nom indique
Collect, Sorted, Values, Alltravailler 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é, mais append(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) puis append. Laisse n zéros au début.
  • Indice hors limites. s[len(s)] provoque un panic. Le dernier élément est s[len(s)-1].
  • Comparer des slices avec ==. Seul s == nil compile. Utilisez slices.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 [].

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER