
Création et gestion des goroutines
Maîtrisez la création et la gestion des goroutines en Go : lancement, fonctions anonymes, WaitGroup, gestion des erreurs, modèles d'annulation et bonnes pratiques pour un code concurrent robuste.
Techniques avancées de création de goroutines : Au-delà du simple go
Si le mot-clé go est la base de la création de goroutines en Go, il existe des techniques plus avancées et idiomatiques pour créer et lancer des goroutines de manière plus flexible et plus structurée, en particulier lorsqu'il s'agit de gérer des scénarios complexes de concurrence, de synchronisation et de gestion du cycle de vie des goroutines.
Ce chapitre explore ces techniques avancées de création et de lancement de goroutines, en allant au-delà du simple go maFonction(). Nous examinerons l'utilisation de fonctions anonymes (closures) pour créer des goroutines "in-line" et capturer des variables de l'environnement extérieur, l'emploi de WaitGroups pour synchroniser et attendre la terminaison d'ensembles de goroutines, les stratégies pour la gestion des erreurs dans les goroutines, et les modèles d'annulation de goroutines pour un contrôle plus fin de leur exécution. L'objectif est de vous outiller avec un éventail de techniques pour créer et gérer les goroutines de manière experte et adaptée aux besoins spécifiques de vos applications concurrentes Go.
Goroutines et fonctions anonymes (closures) : Flexibilité et portée lexicale
L'utilisation de fonctions anonymes (closures) en combinaison avec le mot-clé go offre une grande flexibilité et expressivité pour la création de goroutines en Go. Les fonctions anonymes permettent de définir des goroutines "in-line", directement à l'endroit où elles sont lancées, et de capturer des variables de l'environnement lexical environnant, offrant un moyen puissant de passer des données et de partager un contexte entre la goroutine appelante et la goroutine lancée.
Avantages des goroutines avec fonctions anonymes :
- Définition in-line : Les fonctions anonymes permettent de définir le code de la goroutine directement à l'endroit de son lancement, améliorant la localité et la lisibilité du code, en particulier pour les goroutines courtes et spécifiques à un contexte local.
- Capture de variables de l'environnement : Les closures permettent à la goroutine de capturer et d'accéder aux variables de l'environnement lexical de la fonction appelante. Cela facilite le passage de données à la goroutine et le partage d'un contexte entre la goroutine appelante et la goroutine lancée. Les variables capturées sont partagées par référence, ce qui signifie que les modifications apportées à ces variables dans la goroutine peuvent être visibles dans la fonction appelante (et vice versa, attention aux race conditions !).
- Réduction de la verbosité : Pour les goroutines simples et courtes, l'utilisation de fonctions anonymes peut être plus concise et moins verbeuse que de définir une fonction nommée séparément et de l'appeler ensuite avec
go.
Exemple de goroutines avec fonctions anonymes :
package main
import (
"fmt"
"time"
)
func main() {
message := "Bonjour depuis la goroutine anonyme"
// Lancement d'une goroutine avec une fonction anonyme (closure)
go func() {
fmt.Println("Goroutine anonyme :", message) // Capture de la variable 'message' de l'environnement extérieur
}() // Appel immédiat de la fonction anonyme (et lancement de la goroutine)
fmt.Println("Programme principal continue...")
time.Sleep(1 * time.Second)
}
Dans cet exemple :
- Une fonction anonyme est définie directement comme argument du mot-clé
go. Cette fonction anonyme sera exécutée dans une nouvelle goroutine. - La fonction anonyme capture la variable
messagede l'environnement extérieur (la fonctionmain). Elle peut accéder à cette variable et afficher sa valeur. - L'appel de la fonction anonyme est suivi de
()pour l'exécuter immédiatement (et lancer la goroutine).
L'utilisation de goroutines avec des fonctions anonymes est une technique courante et idiomatique en Go, en particulier pour les tâches concurrentes simples et contextuelles.
WaitGroup : Synchroniser et attendre la fin d'un groupe de goroutines
Dans de nombreux scénarios concurrents, il est nécessaire d'attendre que plusieurs goroutines se terminent avant de poursuivre l'exécution du programme principal. Le package sync de la bibliothèque standard Go fournit le type sync.WaitGroup, un outil puissant pour synchroniser et attendre la fin d'un groupe de goroutines.
Fonctionnement de sync.WaitGroup :
sync.WaitGroup fonctionne comme un compteur. Il permet de :
- Ajouter (incrémenter) le compteur : Pour chaque goroutine que vous lancez et que vous souhaitez attendre, vous incrémentez le compteur du
WaitGroupavec la méthodeAdd(delta int).deltaest généralement1(pour ajouter une goroutine), mais peut être supérieur pour ajouter plusieurs goroutines à la fois. - Signaler la fin d'une goroutine (décrémenter) : Chaque goroutine, une fois qu'elle a terminé son travail, doit décrémenter le compteur du
WaitGroupavec la méthodeDone().Done()est équivalent àAdd(-1). - Attendre que le compteur atteigne zéro (attendre la fin de toutes les goroutines) : La fonction
Wait()duWaitGroupbloque l'exécution de la goroutine appelante (généralement la goroutine principale) jusqu'à ce que le compteur duWaitGroupatteigne zéro. Cela signifie que toutes les goroutines ajoutées auWaitGroupont terminé leur exécution et ont appeléDone().
Exemple d'utilisation de sync.WaitGroup :
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Décrémenter le WaitGroup à la sortie de la goroutine (garantit l'exécution même en cas de panic)
fmt.Printf("Worker %d démarré\n", id)
time.Sleep(time.Duration(id) * time.Second) // Simuler un travail
fmt.Printf("Worker %d terminé\n", id)
}
func main() {
var wg sync.WaitGroup // Déclaration d'un WaitGroup
// Lancement de 3 goroutines worker et ajout de chaque goroutine au WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // Incrémenter le WaitGroup avant de lancer la goroutine
go worker(i, &wg)
}
fmt.Println("Programme principal : Attente de la fin des workers...")
wg.Wait() // Bloquer jusqu'à ce que le WaitGroup soit à zéro (tous les workers aient terminé)
fmt.Println("Programme principal : Tous les workers ont terminé.")
}
Dans cet exemple :
- Un
sync.WaitGroupest déclaré (var wg sync.WaitGroup). - Une boucle
forlance 3 goroutinesworker. Avant de lancer chaque goroutine,wg.Add(1)est appelé pour incrémenter le compteur duWaitGroup. - La fonction
workerprend un*sync.WaitGroupen argument (pour pouvoir agir sur le WaitGroup). A l'intérieur deworker,defer wg.Done()est utilisé pour garantir quewg.Done()sera toujours appelé à la sortie de la goroutine, même en cas de panic.wg.Done()décrémente le compteur du WaitGroup. - Après avoir lancé les 3 goroutines,
wg.Wait()est appelé dans la fonctionmain. Cela bloque l'exécution du programme principal jusqu'à ce que le compteur duWaitGroupatteigne zéro (c'est-à-dire que les 3 goroutinesworkeraient toutes appeléwg.Done()). - Une fois que
wg.Wait()se débloque, le programme principal continue son exécution et affiche le message "Programme principal : Tous les workers ont terminé.".
sync.WaitGroup est un outil essentiel pour synchroniser et attendre la terminaison de groupes de goroutines, permettant de coordonner l'exécution concurrente de tâches et de s'assurer que toutes les opérations concurrentes sont terminées avant de poursuivre l'exécution du programme principal.
Gestion des erreurs dans les goroutines : Collecter et propager les erreurs
La gestion des erreurs dans les goroutines requiert une attention particulière, car les erreurs qui se produisent à l'intérieur d'une goroutine ne sont pas automatiquement propagées à la goroutine appelante (généralement la goroutine principale). Si une goroutine déclenche une panique, cela peut potentiellement faire planter toute l'application, à moins que la panique ne soit récupérée (recovered) de manière contrôlée.
Il est donc important de mettre en place des mécanismes pour collecter les erreurs qui se produisent dans les goroutines et les propager à la goroutine principale (ou à un autre point de gestion des erreurs) afin de pouvoir les traiter de manière appropriée (logging, retries, fallback, etc.).
Techniques pour la gestion des erreurs dans les goroutines :
- Channels pour retourner les erreurs : La méthode la plus idiomatique et la plus sûre pour propager les erreurs depuis une goroutine vers la goroutine appelante est d'utiliser un channel pour retourner la valeur
error. La goroutine worker envoie la valeurerror(ounilen cas de succès) sur un channel, et la goroutine appelante reçoit cette valeur depuis le channel et peut la traiter. - Fonctions de callback avec gestion d'erreurs : Dans certains cas, vous pouvez passer une fonction de callback à une goroutine, qui sera exécutée par la goroutine une fois son travail terminé. La fonction de callback peut prendre en argument une valeur
error(retournée par la goroutine) et gérer l'erreur dans le contexte de la goroutine appelante. - Groupes d'erreurs (Error Groups) : Pour gérer les erreurs provenant de plusieurs goroutines, vous pouvez utiliser un groupe d'erreurs (error group) qui permet de collecter les erreurs de toutes les goroutines et de retourner une erreur agrégée (par exemple, un slice d'erreurs) à la fin de l'exécution des goroutines. Le package
go.sync/errgrouppropose une implémentation de ErrorGroup. - Récupération de paniques (recover) dans les goroutines (avec prudence) : Dans certains cas exceptionnels, vous pouvez utiliser la fonction
recover()à l'intérieur d'une fonctiondeferdans une goroutine pour intercepter une panique et éviter que la goroutine (ou toute l'application) ne plante brutalement. Cependant, l'utilisation derecoverdoit être faite avec prudence et réservée aux cas où vous avez une stratégie claire pour gérer la panique de manière contrôlée. Evitez de masquer ou d'ignorer les paniques sans les comprendre et les traiter correctement.
Exemple de gestion des erreurs avec channels :
package main
import (
"fmt"
"errors"
)
func workerRisque(id int, resultats chan<- string, erreurs chan<- error) {
// Simuler une opération risquée qui peut échouer
if id%2 == 0 {
erreurs <- errors.New(fmt.Sprintf("Worker %d : Erreur lors du traitement", id)) // Envoi d'une erreur sur le channel 'erreurs'
return
}
resultats <- fmt.Sprintf("Worker %d : Traitement réussi", id) // Envoi d'un résultat sur le channel 'resultats'
}
func main() {
resultatsChan := make(chan string)
erreursChan := make(chan error)
// Lancement de 3 goroutines workerRisque
for i := 1; i <= 3; i++ {
go workerRisque(i, resultatsChan, erreursChan)
}
// Collecte des résultats et des erreurs depuis les channels
for i := 1; i <= 3; i++ {
select {
case resultat := <-resultatsChan:
fmt.Println("Résultat reçu :", resultat)
case err := <-erreursChan:
fmt.Println("Erreur reçue :", err)
}
}
}
Dans cet exemple, chaque goroutine workerRisque utilise deux channels : resultatsChan pour retourner un résultat en cas de succès, et erreursChan pour retourner une erreur en cas d'échec. La fonction main utilise une boucle select pour attendre et traiter les résultats ou les erreurs provenant des goroutines, en les collectant depuis les channels appropriés.
Patterns d'annulation de goroutines : Context et contrôle de l'exécution
Dans les applications concurrentes complexes, il est souvent nécessaire de pouvoir annuler l'exécution de goroutines en cours, par exemple, en cas d'erreur, de timeout, d'annulation par l'utilisateur, ou de fermeture du programme. Go propose le package context, un outil puissant et idiomatique pour gérer l'annulation et la propagation de signaux d'annulation à travers les goroutines.
Le package context : Gestion de l'annulation et des délais
Le package context introduit le type context.Context, qui permet de transporter des informations de contexte (comme des deadlines, des valeurs clés-valeur, et des signaux d'annulation) à travers les appels de fonctions et les goroutines. Il fournit des mécanismes pour :
- Annulation : Permettre à une goroutine d'être annulée depuis l'extérieur, en signalant qu'elle doit arrêter son travail et se terminer proprement.
- Délais (Timeouts) : Définir des délais (timeouts) pour les opérations concurrentes, pour limiter leur durée d'exécution et éviter les blocages infinis.
- Valeurs contextuelles (Context Values) : Transporter des valeurs (clés-valeur) à travers la chaîne d'appels de fonctions et les goroutines, pour transmettre des informations contextuelles (par exemple, des identifiants de requête, des informations d'authentification) sans avoir à les passer explicitement comme arguments à chaque fonction.
Création d'un contexte annulable : context.WithCancel
Pour créer un contexte annulable, vous utilisez la fonction context.WithCancel(parent Context) (ctx Context, cancel CancelFunc). Elle prend en argument un contexte parent (généralement context.Background() pour un contexte racine) et retourne :
- Un nouveau contexte enfant
ctx, dérivé du contexte parent, qui est annulable. - Une fonction
cancelde typeCancelFunc. Appeler la fonctioncancel()annule le contextectxet tous ses contextes enfants.
Propagation du signal d'annulation : Channel Done()
Un contexte annulable (créé avec context.WithCancel) expose une méthode Done() <-chan struct{} qui retourne un channel "done". Ce channel est fermé lorsque le contexte est annulé (via l'appel de la fonction cancel()). Les goroutines peuvent écouter ce channel Done() pour détecter le signal d'annulation et arrêter leur travail.
Exemple d'annulation de goroutines avec context.WithCancel :
package main
import (
"context"
"fmt"
"time"
)
func workerAnnulable(ctx context.Context, id int) {
fmt.Printf("Worker %d démarré\n", id)
defer fmt.Printf("Worker %d terminé\n", id)
for {
select {
case <-ctx.Done(): // Ecoute du channel 'Done()' pour détecter l'annulation
fmt.Printf("Worker %d : Annulation détectée, arrêt du travail.\n", id)
return // Sortie de la goroutine
default:
fmt.Printf("Worker %d : Travail en cours...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background()) // Création d'un contexte annulable
// Lancement de 3 goroutines annulables
for i := 1; i <= 3; i++ {
go workerAnnulable(ctx, i)
}
time.Sleep(2 * time.Second) // Laisser les workers travailler pendant 2 secondes
fmt.Println("Programme principal : Annulation des workers...")
cancel() // Annulation du contexte (et de toutes les goroutines qui l'écoutent)
time.Sleep(1 * time.Second) // Laisser le temps aux workers de s'arrêter proprement
fmt.Println("Programme principal : Fin du programme.")
}
Dans cet exemple :
context.WithCancel(context.Background())crée un contexte annulablectxet une fonctioncancel.- Chaque goroutine
workerAnnulableprend le contextectxen argument et écoute le channelctx.Done()dans une boucleselect. - Dans la fonction
main, après 2 secondes,cancel()est appelé pour annuler le contexte. Cela ferme le channelctx.Done(). - Les goroutines
workerAnnulabledétectent la fermeture du channelctx.Done()dans leur boucleselect(cas<-ctx.Done():) et arrêtent leur travail proprement en sortant de la fonction.
Le package context offre un mécanisme puissant et idiomatique pour gérer l'annulation des goroutines et contrôler leur cycle de vie, permettant de construire des applications concurrentes plus robustes et réactives.