Contactez-nous

Itérer sur les slices (et autres collections) avec `range`

Découvrez comment utiliser la boucle `for range` en Go pour itérer élégamment sur les slices, arrays, maps, strings et channels, en obtenant indice et/ou valeur.

Parcourir les collections : La puissance de `for range`

Une opération extrêmement courante en programmation est de parcourir les éléments d'une collection (comme une slice, un tableau, une map, ou même une chaîne de caractères) pour effectuer une action sur chacun d'eux. Bien qu'il soit possible d'utiliser une boucle `for` classique avec un indice pour les slices et les tableaux, Go propose une construction plus élégante, plus lisible et souvent moins sujette aux erreurs pour l'itération : la boucle `for range`.

Cette forme spéciale de la boucle `for` est conçue spécifiquement pour l'itération sur diverses structures de données. Elle simplifie le processus en fournissant directement l'indice et/ou la valeur de chaque élément à chaque itération, masquant les détails de l'accès par indice ou de la gestion des pointeurs internes.

Maîtriser `for range` est essentiel pour écrire du code Go idiomatique et efficace lorsque vous travaillez avec des collections. Nous allons explorer ici son utilisation principale avec les slices, et mentionner brièvement son application à d'autres types.

Syntaxe de `for range` avec les Slices (et Tableaux)

Lorsqu'elle est utilisée avec une slice ou un tableau, la boucle `for range` retourne deux valeurs à chaque itération : l'indice de l'élément et une copie de la valeur de l'élément à cet indice.

La syntaxe générale est :

maSlice := []string{"un", "deux", "trois"}

for index, value := range maSlice {
    fmt.Printf("Index: %d, Valeur: %s\n", index, value)
}
Dans cet exemple :
  • `range maSlice` : Indique que nous voulons itérer sur la slice `maSlice`.
  • `index, value := ...` : A chaque itération, l'indice de l'élément courant est assigné à la variable `index`, et une copie de la valeur de l'élément est assignée à la variable `value`.
La sortie sera :
Index: 0, Valeur: un
Index: 1, Valeur: deux
Index: 2, Valeur: trois

Si vous n'avez besoin que de l'indice, vous pouvez omettre la variable de valeur :

for index := range maSlice {
    fmt.Printf("Index: %d\n", index)
}
La sortie sera :
Index: 0
Index: 1
Index: 2

Si vous n'avez besoin que de la valeur, vous devez explicitement ignorer l'indice en utilisant l'identifiant vide `_` :

somme := 0
nombres := []int{10, 20, 30}

for _, valeur := range nombres {
    somme += valeur // Ajoute la copie de la valeur à la somme
}
fmt.Println("Somme:", somme) // Affiche: Somme: 60
Ici, `_` indique que nous ne sommes pas intéressés par l'indice pour cette itération.

Point crucial : La variable `value` est une copie

Il est fondamental de comprendre que la variable de valeur (le `value` dans `index, value := range ...` ou `_, value := range ...`) reçoit une copie de l'élément de la collection à chaque itération, et non une référence directe à l'élément original dans la slice (pour les types valeur comme int, string, bool, struct...).

Cela a une implication importante : si vous essayez de modifier l'élément de la slice en modifiant directement la variable `value` de la boucle, cela ne fonctionnera pas comme vous pourriez l'attendre. Vous ne modifierez que la copie locale à l'itération courante, pas l'élément dans la slice d'origine.

Considérez cet exemple incorrect :

package main

import "fmt"

func main() {
    nombres := []int{1, 2, 3}
    fmt.Println("Avant modification (tentative échouée):", nombres)

    for _, val := range nombres { 
        val = val * 2 // Modifie UNIQUEMENT la copie 'val'
    }

    // La slice 'nombres' originale n'a PAS été modifiée !
    fmt.Println("Après modification (tentative échouée):", nombres) // Affiche: [1 2 3]
}
La raison est que, pour chaque itération, la boucle `range` copie la valeur de `nombres[0]`, `nombres[1]`, etc., dans la même variable `val`. Modifier `val` ne fait que changer cette copie temporaire.

Pour modifier les éléments de la slice originale pendant l'itération, vous devez utiliser l'indice pour accéder directement à l'élément dans la slice :

package main

import "fmt"

func main() {
    nombres := []int{1, 2, 3}
    fmt.Println("Avant modification (correcte):", nombres)

    for i := range nombres { // On récupère l'indice 'i'
        nombres[i] = nombres[i] * 2 // Modifie l'élément DANS la slice via son indice
    }

    // La slice 'nombres' originale EST modifiée !
    fmt.Println("Après modification (correcte):", nombres) // Affiche: [2 4 6]
}
C'est la manière idiomatique et correcte de modifier les éléments d'une slice lors d'une itération avec `for range`.

Itérer sur d'autres types avec `range`

La boucle `for range` n'est pas limitée aux slices et tableaux. Elle fonctionne également de manière idiomatique avec d'autres types de collections intégrés :

  1. Strings : Itérer sur une chaîne avec `range` parcourt ses runes (points de code Unicode), pas ses bytes individuels. Elle retourne l'indice du début du byte de la rune et la rune elle-même (de type `rune`, un alias pour `int32`).
    for index, charRune := range "Go语言" {
        fmt.Printf("Byte Index: %d, Rune: %c, Unicode: %U\n", index, charRune, charRune)
    }
    // Sortie :
    // Byte Index: 0, Rune: G, Unicode: U+0047
    // Byte Index: 1, Rune: o, Unicode: U+006F
    // Byte Index: 2, Rune: 语, Unicode: U+8BED
    // Byte Index: 5, Rune: 言, Unicode: U+8A00
    // (Notez que les runes non-ASCII occupent plusieurs bytes, d'où le saut d'index)
  2. Maps : Itérer sur une map retourne la clé et la valeur de chaque paire. Important : L'ordre d'itération sur une map n'est pas garanti en Go ; il peut varier d'une exécution à l'autre.
    ages := map[string]int{"Alice": 30, "Bob": 25}
    
    for nom, age := range ages {
        fmt.Printf("%s a %d ans.\n", nom, age)
    }
    // Sortie possible (l'ordre peut varier) :
    // Alice a 30 ans.
    // Bob a 25 ans.
    Si vous avez besoin d'itérer sur une map dans un ordre spécifique (par exemple, alphabétique), vous devez d'abord extraire les clés dans une slice, trier cette slice, puis itérer sur la slice triée pour accéder aux éléments de la map.
  3. Channels : Itérer sur un channel avec `range` permet de recevoir des valeurs du channel jusqu'à ce qu'il soit fermé. Nous aborderons cela plus en détail dans la partie sur la concurrence.
    // Exemple conceptuel (nécessite des goroutines et la fermeture du channel)
    // ch := make(chan int)
    // ... (quelque chose envoie des valeurs sur ch et le ferme)
    // for valeur := range ch {
    //     fmt.Println("Reçu:", valeur)
    // }

Conclusion : L'itération idiomatique en Go

La boucle `for range` est un outil puissant et fondamental dans la boîte à outils du développeur Go. Elle offre une syntaxe claire et concise pour parcourir les éléments des structures de données les plus courantes : slices, tableaux, maps, strings et channels.

Ses principaux avantages sont la lisibilité et la réduction des erreurs potentielles liées à la gestion manuelle des indices (comme les erreurs "off-by-one"). N'oubliez pas le comportement clé : la variable `value` est une copie de l'élément. Pour modifier la collection originale (comme une slice) pendant l'itération, utilisez l'indice fourni par `range` pour y accéder directement.

Adopter `for range` comme méthode d'itération par défaut lorsque c'est approprié rendra votre code Go plus facile à lire, à écrire et à maintenir.