
La boucle `for range` sur les canaux
Découvrez comment utiliser `for range` pour recevoir élégamment des valeurs d'un canal Go jusqu'à sa fermeture, simplifiant ainsi les boucles de réception concurrentes.
Recevoir en continu : Simplifier l'itération sur les canaux
Nous avons vu comment envoyer (`<-`) et recevoir (`<-`) des valeurs individuelles sur des canaux, ainsi que comment détecter la fermeture d'un canal en utilisant la forme `valeur, ok := <- monCanal`. Cependant, un scénario très courant est celui où une goroutine doit recevoir et traiter *toutes* les valeurs envoyées sur un canal jusqu'à ce que celui-ci soit fermé par l'expéditeur.
Bien qu'on puisse implémenter cela avec une boucle `for` infinie et le test `if !ok { break }`, Go offre une syntaxe beaucoup plus élégante et idiomatique pour ce cas précis : l'utilisation de la boucle `for range` directement sur le canal.
Cette construction simplifie grandement le code du récepteur, en gérant automatiquement la réception des valeurs et la détection de la fermeture du canal.
Syntaxe et Comportement
La syntaxe pour utiliser `for range` sur un canal est très simple :
for valeur := range nomCanal {
// Bloc exécuté pour chaque 'valeur' reçue du 'nomCanal'
// ... traiter valeur ...
}Le comportement de cette boucle est le suivant :
1. La boucle bloque à chaque itération, en attendant qu'une valeur soit envoyée sur `nomCanal`.
2. Lorsqu'une valeur est reçue, elle est assignée à la variable `valeur` (ici, nommée `valeur`, mais vous pouvez choisir n'importe quel nom valide).
3. Le corps de la boucle est exécuté avec cette valeur.
4. La boucle retourne à l'étape 1 pour attendre la valeur suivante.
5. Crucial : La boucle `for range` sur un canal se termine automatiquement et proprement lorsque le canal `nomCanal` est fermé (`close(nomCanal)`) par l'expéditeur et que toutes les valeurs éventuellement bufferisées ont été reçues.
Cette gestion automatique de la fermeture est ce qui rend cette construction si pratique. Elle remplace élégamment la boucle `for { val, ok := <-ch; if !ok { break }; ... }`.
Exemple pratique : Producteur / Consommateur
Illustrons cela avec un exemple classique où une goroutine produit des données (les envoie sur un canal) et la goroutine principale les consomme en utilisant `for range`.
package main
import (
"fmt"
"time"
)
func main() {
// Canal pour envoyer des 'tâches' (ici, des entiers)
taches := make(chan int, 3) // Canal bufferisé pour montrer qu'il se vide avant de terminer
// Goroutine Producteur
go func() {
// Important: L'expéditeur doit fermer le canal !
defer close(taches)
for i := 1; i <= 5; i++ {
fmt.Printf("(Producteur) Envoi de la tâche %d\n", i)
taches <- i
time.Sleep(100 * time.Millisecond)
}
fmt.Println("(Producteur) Plus de tâches à envoyer.")
}()
// Goroutine principale (Consommateur)
fmt.Println("(Consommateur) En attente de tâches...")
// Boucle for range sur le canal 'taches'
for tache := range taches {
fmt.Printf("(Consommateur) Traitement de la tâche %d\n", tache)
time.Sleep(200 * time.Millisecond) // Simule le traitement
}
// Le code ici n'est atteint que lorsque le canal 'taches' est fermé
// ET que la boucle for range a fini de recevoir toutes les valeurs.
fmt.Println("(Consommateur) Canal fermé. Boucle terminée. Fin du programme.")
}Sortie probable :
(Consommateur) En attente de tâches...
(Producteur) Envoi de la tâche 1
(Consommateur) Traitement de la tâche 1
(Producteur) Envoi de la tâche 2
(Consommateur) Traitement de la tâche 2
(Producteur) Envoi de la tâche 3
(Consommateur) Traitement de la tâche 3
(Producteur) Envoi de la tâche 4
(Consommateur) Traitement de la tâche 4
(Producteur) Envoi de la tâche 5
(Producteur) Plus de tâches à envoyer.
(Consommateur) Traitement de la tâche 5
(Consommateur) Canal fermé. Boucle terminée. Fin du programme.Dans cet exemple, la boucle `for tache := range taches` dans `main` reçoit les valeurs 1 à 5. Lorsque la goroutine producteur appelle `close(taches)`, la boucle `for range` reçoit les dernières valeurs éventuellement en attente (ici, la 5), puis détecte que le canal est fermé et se termine proprement. L'exécution continue alors après la boucle.
Condition de terminaison : La fermeture du canal est essentielle
Il est absolument critique de comprendre que la boucle `for range` sur un canal ne se termine que si le canal est fermé. Si l'expéditeur (ou les expéditeurs) ne ferme jamais le canal, mais arrête simplement d'envoyer des valeurs, la boucle `for range` restera bloquée indéfiniment, attendant de recevoir une autre valeur.
C'est ce qu'on appelle un deadlock : une situation où une ou plusieurs goroutines sont bloquées et attendent un événement qui ne se produira jamais. Si la goroutine principale est bloquée dans un `for range` sur un canal qui n'est jamais fermé, le runtime Go détectera généralement ce deadlock et arrêtera le programme avec une panique.
Par conséquent, lorsque vous utilisez `for range` pour consommer les valeurs d'un canal, vous devez vous assurer qu'une autre partie de votre programme (typiquement, la ou les goroutines qui envoient sur ce canal) appellera `close()` sur ce canal à un moment donné pour signaler la fin du flux de données.
Conclusion : L'outil idiomatique pour la consommation de canaux
La boucle `for range` appliquée aux canaux est un outil puissant et élégant de l'arsenal concurrentiel de Go. Elle fournit une manière concise, lisible et sûre de recevoir toutes les valeurs d'un canal jusqu'à sa fermeture.
Elle simplifie le code du récepteur en gérant automatiquement le blocage en attente de valeurs et la détection de la fermeture du canal, éliminant le besoin de la vérification manuelle `val, ok := <-ch` dans une boucle `for {}`.
C'est la méthode idiomatique à privilégier pour consommer des données provenant d'un canal lorsque vous vous attendez à recevoir plusieurs valeurs et que l'expéditeur signalera la fin en fermant le canal. N'oubliez jamais la nécessité cruciale de fermer le canal pour permettre à la boucle `for range` de se terminer.