Contactez-nous

Arrays et Slices

Maîtrisez arrays et slices en Go : structures de données séquentielles essentielles. Découvrez déclaration, initialisation, manipulation, différences clés et cas d'usage pour optimiser vos développements Go.

Introduction aux Arrays et Slices : Séquences de données en Go

Dans le vaste univers de la programmation, les arrays et les slices occupent une place centrale en tant que structures de données fondamentales pour organiser et manipuler des séquences d'éléments. Go, avec son approche pragmatique et efficace, propose ces deux types de collections séquentielles, chacun répondant à des besoins spécifiques en termes de taille, de flexibilité et de performance.

Imaginez un array comme une rangée de cases mémoire contiguës, chacune capable de stocker une valeur d'un type donné. La caractéristique essentielle d'un array est sa taille fixe, définie lors de sa déclaration et immuable par la suite. Les slices, quant à eux, se présentent comme des abstractions plus dynamiques et flexibles au-dessus des arrays. Un slice offre une vue sur une portion d'un array sous-jacent (ou sur un array anonyme), et sa taille peut varier dynamiquement au cours de l'exécution du programme.

Ce chapitre se consacre à une exploration approfondie des arrays et des slices en Go. Nous allons disséquer leur syntaxe de déclaration, leurs mécanismes de fonctionnement interne, leurs opérations de manipulation courantes, leurs différences clés, leurs cas d'utilisation privilégiés et les meilleures pratiques pour les employer efficacement dans vos projets Go. Que vous soyez débutant ou développeur expérimenté, ce guide complet vous permettra de maîtriser ces structures de données essentielles et de les utiliser à bon escient pour construire des applications Go robustes et performantes.

Arrays en Go : Collections de taille fixe

Les arrays en Go sont des structures de données qui représentent des séquences d'éléments de même type, avec une taille fixe déterminée à la compilation. La taille d'un array fait partie intégrante de son type, ce qui signifie qu'un array de taille 5 est d'un type différent d'un array de taille 10, même s'ils contiennent des éléments du même type.

Déclaration d'un array :

La syntaxe de déclaration d'un array en Go spécifie à la fois le type des éléments et la taille de l'array entre crochets [].

var nomArray [taille]typeElement

  • var nomArray : Le nom de la variable array que vous déclarez.
  • [taille] : La taille de l'array, un entier constant positif spécifiant le nombre d'éléments que l'array peut contenir. La taille doit être connue à la compilation.
  • typeElement : Le type de données des éléments que l'array va stocker (par exemple, int, string, float64, etc.).

Initialisation d'un array :

Lorsqu'un array est déclaré sans initialisation explicite, ses éléments sont initialisés à la valeur zéro du type de l'élément (0 pour les entiers, 0.0 pour les flottants, "" pour les strings, false pour les booléens, nil pour les pointeurs, etc.).

Vous pouvez initialiser un array lors de sa déclaration de plusieurs manières :

  • Initialisation littérale avec des valeurs :
      var nombres [5]int = [5]int{10, 20, 30, 40, 50}
      // Ou, en inférant le type et la taille : (plus idiomatique)
      nombres := [5]int{10, 20, 30, 40, 50}
      // Ou, en omettant la taille (elle est inférée du nombre d'éléments) : (encore plus concis)
      nombres := [...]int{10, 20, 30, 40, 50}
      
  • Initialisation partielle avec des index :
      nombres := [5]int{0: 10, 3: 40} // Initialise l'élément à l'index 0 à 10, l'élément à l'index 3 à 40,
                                       // et les autres éléments à la valeur zéro (0 pour int)
      

Accès aux éléments d'un array :

Les éléments d'un array sont accessibles via leur index, qui commence à 0 pour le premier élément et va jusqu'à taille-1 pour le dernier élément. L'accès à un élément se fait en utilisant l'index entre crochets [] après le nom de l'array.

package main

import "fmt"

func main() {
    nombres := [5]int{10, 20, 30, 40, 50}

    fmt.Println("Premier élément :", nombres[0]) // Accès à l'élément à l'index 0 (10)
    fmt.Println("Troisième élément :", nombres[2]) // Accès à l'élément à l'index 2 (30)
    fmt.Println("Dernier élément :", nombres[4])  // Accès à l'élément à l'index 4 (50)

    // Modification d'un élément
    nombres[1] = 25
    fmt.Println("Array après modification du deuxième élément :", nombres)
}

Limitations des arrays :

Les arrays en Go, en raison de leur taille fixe, présentent certaines limitations :

  • Taille fixe immuable : La taille d'un array est fixée à la déclaration et ne peut pas être modifiée dynamiquement. Si vous avez besoin d'une collection de taille variable, vous devez utiliser un slice.
  • Passage par valeur lors de la copie : Lorsqu'un array est passé en argument à une fonction ou assigné à une autre variable, il est copié en entier (passage par valeur). Pour les grands arrays, cela peut être coûteux en termes de performance et de mémoire. Dans de tels cas, il est préférable d'utiliser des slices, qui sont passés par référence (en réalité, une copie du descripteur du slice, mais pas des données sous-jacentes).
  • Type dépendant de la taille : Comme mentionné précédemment, le type d'un array inclut sa taille. Vous ne pouvez pas directement utiliser une fonction qui attend un [5]int avec un [10]int, même s'ils contiennent des entiers.

En raison de ces limitations, les arrays sont moins fréquemment utilisés directement dans le code Go courant que les slices, qui offrent plus de flexibilité et de dynamisme. Cependant, les arrays restent importants en tant que base sous-jacente pour les slices et dans certains cas spécifiques où une taille fixe et connue à la compilation est requise.

Slices en Go : Vues dynamiques sur des séquences

Les slices en Go sont des abstractions puissantes et flexibles pour travailler avec des séquences de données. Un slice est une structure de données à trois composants qui décrit une section contiguë d'un array sous-jacent (qui peut être anonyme) :

  • Pointeur : Un pointeur vers le premier élément du segment de l'array accessible via le slice. Si le slice est nil, le pointeur est nil.
  • Longueur (len) : Le nombre d'éléments que le slice contient actuellement. C'est la longueur de la "vue" que le slice offre sur l'array sous-jacent.
  • Capacité (cap) : Le nombre total d'éléments dans l'array sous-jacent, à partir de l'élément pointé par le pointeur du slice, jusqu'à la fin de l'array. La capacité définit la taille maximale que le slice peut atteindre sans réallocation.

Contrairement aux arrays, les slices ont une taille dynamique. Vous pouvez ajouter ou supprimer des éléments d'un slice (dans la limite de sa capacité) et sa longueur s'ajustera en conséquence. Les slices sont beaucoup plus couramment utilisés que les arrays dans la programmation Go quotidienne en raison de leur flexibilité.

Déclaration d'un slice :

La syntaxe de déclaration d'un slice est similaire à celle d'un array, mais sans spécifier la taille entre crochets [].

var nomSlice []typeElement

  • var nomSlice : Le nom de la variable slice que vous déclarez.
  • [] : Les crochets vides [] indiquent qu'il s'agit d'un slice (et non d'un array).
  • typeElement : Le type de données des éléments que le slice va contenir.

Initialisation d'un slice :

Un slice peut être initialisé de différentes manières :

  • Slice nil : Un slice déclaré sans initialisation explicite a la valeur nil. Un slice nil n'a pas d'array sous-jacent, sa longueur et sa capacité sont nulles. Il est valide d'ajouter des éléments à un slice nil avec la fonction append (ce qui provoquera l'allocation d'un array sous-jacent).
  • Slice vide (non-nil) : Vous pouvez initialiser un slice vide (non-nil) en utilisant la syntaxe littérale vide []typeElement{} ou avec la fonction make avec une longueur de 0. Un slice vide a un array sous-jacent (de taille 0), mais sa longueur est 0.
  • Initialisation littérale avec des valeurs : Similaire à l'initialisation des arrays, mais sans spécifier la taille. La taille du slice est inférée du nombre d'éléments.
      nombres := []int{1, 2, 3, 4, 5} // Crée un slice de longueur 5 et capacité 5
      
  • Création d'un slice à partir d'un array (slicing) : L'opération de slicing permet de créer un nouveau slice qui "vue" une portion d'un array existant (ou d'un autre slice). La syntaxe de slicing utilise la notation [début:fin] ou [début:fin:capacitéMax].
      array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
      slice1 := array[2:5]    // slice1 vue sur array à partir de l'index 2 (inclus) jusqu'à l'index 5 (exclu)
                              // longueur de slice1 : 5-2 = 3, capacité de slice1 : 10-2 = 8
      slice2 := array[:3]     // slice2 vue sur array du début jusqu'à l'index 3 (exclu)
                              // longueur de slice2 : 3-0 = 3, capacité de slice2 : 10-0 = 10
      slice3 := array[7:]     // slice3 vue sur array de l'index 7 (inclus) jusqu'à la fin
                              // longueur de slice3 : 10-7 = 3, capacité de slice3 : 10-7 = 3
      slice4 := array[:]     // slice4 vue sur tout l'array
                              // longueur de slice4 : 10, capacité de slice4 : 10
      slice5 := array[2:5:7] // slice5 vue sur array de l'index 2 à 5 (exclu), capacité limitée à l'index 7 (exclu)
                              // longueur de slice5 : 5-2 = 3, capacité de slice5 : 7-2 = 5
      
  • Création d'un slice avec make : La fonction intégrée make([]typeElement, longueur, capacité) permet de créer un slice en allouant un array sous-jacent de la capacité spécifiée et en créant un slice de longueur initiale pointant vers ce array. Si la capacité est omise, elle est par défaut égale à la longueur.
      slice1 := make([]int, 5)        // slice1 de longueur 5 et capacité 5 (array sous-jacent de taille 5)
      slice2 := make([]string, 0, 10) // slice2 de longueur 0 et capacité 10 (array sous-jacent de taille 10, slice vide au départ)
      

Longueur et capacité d'un slice : fonctions len et cap

Les fonctions intégrées len(slice) et cap(slice) permettent d'obtenir respectivement la longueur et la capacité d'un slice.

package main

import "fmt"

func main() {
    slice := make([]int, 3, 10)
    fmt.Println("Slice :", slice)         // Affiche [0 0 0] (valeurs zéro par défaut)
    fmt.Println("Longueur :", len(slice))   // Affiche 3
    fmt.Println("Capacité :", cap(slice))   // Affiche 10
}

Ajout d'éléments à un slice : fonction append

La fonction intégrée append(slice, elements...) permet d'ajouter de nouveaux éléments à la fin d'un slice. append retourne un nouveau slice (il est donc important de réassigner le résultat à la variable slice originale).

Si l'ajout d'éléments dépasse la capacité actuelle du slice, append réalloue automatiquement un nouvel array sous-jacent plus grand (généralement le double de la capacité précédente) et copie les éléments existants dans le nouveau array. Le slice retourné pointera vers ce nouvel array.

package main

import "fmt"

func main() {
    slice := make([]int, 0, 3) // Slice vide avec capacité 3
    fmt.Println("Slice initial :", slice, "Longueur :", len(slice), "Capacité :", cap(slice))

    slice = append(slice, 10) // Ajout d'un élément
    fmt.Println("Slice après append(10) :", slice, "Longueur :", len(slice), "Capacité :", cap(slice))

    slice = append(slice, 20, 30) // Ajout de plusieurs éléments
    fmt.Println("Slice après append(20, 30) :", slice, "Longueur :", len(slice), "Capacité :", cap(slice))

    slice = append(slice, 40) // Ajout d'un élément qui dépasse la capacité initiale
    fmt.Println("Slice après append(40) (dépasse capacité) :", slice, "Longueur :", len(slice), "Capacité :", cap(slice)) // Capacité doublée
}

Copie de slices : fonction copy

La fonction intégrée copy(destinationSlice, sourceSlice) permet de copier des éléments d'un sourceSlice vers un destinationSlice. copy retourne le nombre d'éléments copiés (qui est le minimum entre la longueur du destinationSlice et du sourceSlice).

package main

import "fmt"

func main() {
    source := []int{1, 2, 3, 4, 5}
    destination := make([]int, 3) // Destination de longueur 3

    elementsCopied := copy(destination, source) // Copie de 'source' vers 'destination'
    fmt.Println("Source :", source)
    fmt.Println("Destination après copie :", destination)
    fmt.Println("Eléments copiés :", elementsCopied) // Affiche 3 (longueur de destination)
}

Itération sur un slice : boucle for...range

La boucle for...range est la manière idiomatique d'itérer sur les éléments d'un slice (ou d'un array, d'une map, d'une chaîne de caractères). Elle fournit à chaque itération l'index et la valeur de l'élément courant.

package main

import "fmt"

func main() {
    nombres := []int{100, 200, 300}

    for index, valeur := range nombres {
        fmt.Printf("Index : %d, Valeur : %d\n", index, valeur)
    }

    // Si vous n'avez besoin que des valeurs (et pas des index), vous pouvez ignorer l'index avec l'identifiant blanc '_'
    for _, valeur := range nombres {
        fmt.Println("Valeur :", valeur)
    }
}

Slices nil et slices vides : distinctions importantes

  • Slice nil :
    • Un slice nil est un slice qui n'a pas d'array sous-jacent.
    • Sa valeur est nil.
    • Sa longueur et sa capacité sont 0.
    • Il est valide d'utiliser append sur un slice nil (cela allouera un array sous-jacent).
    • Exemple de déclaration : var slice []int (sans initialisation).
  • Slice vide (non-nil) :
    • Un slice vide est un slice qui a un array sous-jacent (même s'il est de taille 0), mais sa longueur est 0.
    • Sa valeur n'est pas nil.
    • Sa capacité peut être 0 ou plus.
    • Il est valide d'utiliser append sur un slice vide.
    • Exemples de déclaration : slice := []int{}, slice := make([]int, 0), slice := make([]int, 0, 10).

La distinction entre slice nil et slice vide est importante, en particulier lors de la sérialisation JSON (un slice nil est sérialisé en null, un slice vide en []). Dans la plupart des cas, un slice vide est plus approprié qu'un slice nil pour représenter une collection vide.

Arrays vs. Slices : Lequel choisir ?

Le choix entre arrays et slices en Go dépend des besoins spécifiques de votre application et des caractéristiques de chaque structure de données.

Choisir un array lorsque :

  • Vous avez besoin d'une collection de taille fixe connue à la compilation et qui ne changera pas pendant l'exécution du programme.
  • La performance est critique et vous voulez éviter les coûts potentiels de réallocation dynamique des slices (bien que les slices soient généralement très performants).
  • Vous travaillez avec du code existant ou des bibliothèques qui attendent ou retournent des arrays de taille fixe.
  • Vous avez besoin d'un type de données qui soit comparable (les arrays sont comparables si les types d'éléments sont comparables, les slices ne le sont pas).

Choisir un slice lorsque :

  • Vous avez besoin d'une collection de taille dynamique qui peut grandir ou rétrécir au cours de l'exécution.
  • La flexibilité et la facilité de manipulation sont importantes.
  • Vous devez passer des collections à des fonctions sans encourir de coûts de copie importants (les slices sont passés par référence).
  • Vous utilisez des fonctions Go standard ou des bibliothèques qui fonctionnent principalement avec des slices (ce qui est le cas de la majorité des API Go).
  • Vous avez besoin d'une abstraction plus générale et plus puissante pour représenter des séquences de données.

En résumé :

  • Utilisez les arrays lorsque vous avez besoin d'une collection de taille fixe et que vous connaissez cette taille à la compilation. Les arrays sont moins courants dans la plupart des applications Go.
  • Utilisez les slices dans la grande majorité des cas. Les slices offrent la flexibilité, le dynamisme et la performance nécessaires pour la plupart des tâches de manipulation de séquences de données en Go. Les slices sont la structure de données séquentielle de choix en Go.

Dans la pratique, vous utiliserez beaucoup plus souvent des slices que des arrays dans vos programmes Go. Les slices sont plus idiomatiques, plus flexibles et répondent mieux aux besoins de la plupart des applications.

Bonnes pratiques pour l'utilisation des Arrays et des Slices

Pour utiliser efficacement les arrays et les slices en Go et écrire du code robuste et performant, voici quelques bonnes pratiques à suivre :

  • Privilégier les slices par défaut : Dans la plupart des cas, utilisez des slices plutôt que des arrays, sauf si vous avez une raison spécifique de choisir un array (taille fixe, performance extrême dans des cas spécifiques).
  • Utiliser make pour initialiser les slices lorsque la taille est connue à l'avance : Si vous connaissez la taille approximative d'un slice que vous allez créer, utilisez make([]type, longueur, capacité) avec une capacité initiale appropriée. Cela peut réduire le nombre de réallocations lors de l'ajout d'éléments avec append et améliorer les performances.
  • Etre conscient de la capacité des slices : Gardez un oeil sur la capacité des slices, en particulier lorsque vous utilisez append dans des boucles. Si vous ajoutez beaucoup d'éléments à un slice, la capacité peut être réallouée plusieurs fois, ce qui peut avoir un impact sur les performances. Dans certains cas, il peut être plus efficace de pré-allouer un slice avec une capacité suffisante dès le départ.
  • Eviter les copies inutiles de grands arrays : Si vous devez passer un grand array à une fonction, envisagez de passer un slice "vue" sur cet array (array[:]) plutôt que l'array entier, pour éviter la copie coûteuse de toutes les données.
  • Gérer correctement les slices nil et vides : Soyez conscient de la différence entre un slice nil et un slice vide. Choisissez le type de slice approprié en fonction de vos besoins (par exemple, utilisez un slice vide pour représenter une collection vide sérialisée en JSON comme []). Vérifiez si un slice est nil si nécessaire avant de l'utiliser (par exemple, avant d'itérer dessus).
  • Utiliser le slicing pour créer des vues et des sous-ensembles : Exploitez la puissance du slicing pour créer des vues sur des portions de données sans copier les données sous-jacentes. Le slicing est un outil efficace pour manipuler des séquences de données de manière flexible et performante.
  • Documenter clairement l'utilisation des arrays et des slices dans votre code : Si vous utilisez des arrays ou des slices de manière spécifique ou non-évidente, documentez votre code pour expliquer les choix que vous avez faits et les raisons qui vous ont poussé à utiliser ces structures de données de cette manière.

En appliquant ces bonnes pratiques, vous utiliserez les arrays et les slices de manière optimale dans vos programmes Go, en tirant parti de leurs avantages tout en évitant les pièges potentiels.