
Une goroutine est une fonction qui s'exécute en parallèle, pour un coût dérisoire ; les channels leur permettent de communiquer sans partager de mémoire. C'est la marque de fabrique de Go. Ce guide vous apprend à lancer des goroutines, à les faire coopérer par channels, à construire un pool de workers et à éviter les deux pièges classiques : le deadlock et la fuite de goroutine. Public visé : vous connaissez les fonctions et les slices. Exemples testés sur Go 1.26.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Lancer une goroutine avec
goet communiquer par channel. - Faire coopérer plusieurs workers sur un même channel.
- Fermer un channel et détecter la fermeture avec
range. - Éviter deadlocks et fuites de goroutines.
Lancer une goroutine
Section intitulée « Lancer une goroutine »Le mot-clé go devant un appel de fonction l'exécute dans une goroutine, sans bloquer l'appelant. Une goroutine est bien plus légère qu'un thread système : on peut en lancer des milliers.
go faireQuelqueChose() // part en parallèle, main continueLe piège immédiat : si main se termine, le programme s'arrête, goroutines en cours comprises. Il faut donc un moyen de coordonner la fin. C'est là qu'interviennent les channels et sync.WaitGroup.
Communiquer par channels
Section intitulée « Communiquer par channels »Un channel est un tuyau typé par lequel les goroutines s'échangent des valeurs. La philosophie Go : « ne communiquez pas en partageant la mémoire, partagez la mémoire en communiquant ». On envoie avec ch <- v et on reçoit avec v := <-ch.
ch := make(chan int)go func() { ch <- 42 }() // envoie dans une goroutinev := <-ch // reçoit (bloque jusqu'à réception)fmt.Println(v) // 42Un channel non bufferisé synchronise : l'envoi bloque jusqu'à ce qu'une réception ait lieu, et inversement. C'est ce rendez-vous qui coordonne les goroutines sans verrou explicite.
Un pool de workers
Section intitulée « Un pool de workers »Le motif le plus utile en pratique : distribuer des tâches à plusieurs workers qui les traitent en parallèle. On combine un channel de jobs, un channel de résultats et un sync.WaitGroup pour attendre la fin.
func main() { jobs := make(chan int, 5) resultats := make(chan int, 5) var wg sync.WaitGroup
for w := 1; w <= 3; w++ { // 3 workers concurrents wg.Add(1) go func() { defer wg.Done() for j := range jobs { // consomme jusqu'à fermeture resultats <- j * j } }() }
for i := 1; i <= 5; i++ { jobs <- i } close(jobs) // signale la fin aux workers
wg.Wait() // attend tous les workers close(resultats) // puis ferme les résultats
total := 0 for r := range resultats { total += r } fmt.Println(total) // 55}Sortie :
55Trois workers se partagent les cinq jobs. close(jobs) signale qu'il n'y aura plus de tâches : le for range de chaque worker se termine alors proprement. On attend tous les workers avec wg.Wait() avant de fermer les résultats, sinon on lirait un channel encore en cours d'écriture.
Fermeture, range et détection
Section intitulée « Fermeture, range et détection »Fermer un channel avec close indique qu'aucune valeur ne viendra plus. Un for v := range ch s'arrête automatiquement à la fermeture ; la réception v, ok := <-ch met ok à false. Deux règles d'or :
- Seul l'émetteur ferme un channel, jamais le récepteur.
- Ne jamais écrire dans un channel fermé (panique immédiate).
- Deadlock : toutes les goroutines sont bloquées en attente mutuelle. Go le détecte souvent au runtime (
all goroutines are asleep - deadlock!). Cause classique : lire un channel que personne ne remplit, ou oublier de le fermer. - Fuite de goroutine : une goroutine reste bloquée pour toujours (en attente sur un channel jamais alimenté). Elle ne plante pas, mais consomme de la mémoire indéfiniment. Le contexte (
context.Context) sert justement à leur signaler d'abandonner.
Exercice : additionner en parallèle
Section intitulée « Exercice : additionner en parallèle »Un exercice de découpage concurrent. Cherchez la solution avant d'ouvrir la réponse.
Additionnez le slice []int{1,2,3,4,5,6,7,8} en parallèle :
- coupez-le en deux moitiés.
- lancez une goroutine par moitié qui calcule sa somme partielle et l'envoie dans un channel.
- attendez les deux goroutines avec un
sync.WaitGroup, puis additionnez les deux résultats partiels.
Le total attendu est 36.
Indice : passez le *sync.WaitGroup par pointeur, et fermez le channel après wg.Wait().
Chaque goroutine somme sa moitié et envoie le partiel ; main combine après avoir attendu.
package main
import ( "fmt" "sync")
func sommer(nombres []int, resultats chan<- int, wg *sync.WaitGroup) { defer wg.Done() s := 0 for _, n := range nombres { s += n } resultats <- s}
func main() { nombres := []int{1, 2, 3, 4, 5, 6, 7, 8} resultats := make(chan int, 2) var wg sync.WaitGroup
milieu := len(nombres) / 2 wg.Add(2) go sommer(nombres[:milieu], resultats, &wg) // 1..4 go sommer(nombres[milieu:], resultats, &wg) // 5..8
wg.Wait() close(resultats)
total := 0 for partiel := range resultats { total += partiel } fmt.Println("somme totale:", total)}Résultat :
somme totale: 36Le channel est bufferisé à 2 pour que les goroutines puissent y déposer leur résultat sans attendre un lecteur. On ne le ferme qu'après wg.Wait(), une fois les deux envois garantis. C'est le squelette de tout traitement parallèle par découpage.
À retenir
Section intitulée « À retenir »- Le mot-clé
golance une goroutine ; elles sont si légères qu'on en lance des milliers. - Un channel (
make(chan T)) fait communiquer les goroutines ; un channel non bufferisé synchronise. - Un pool de workers distribue des jobs via un channel ; on attend la fin avec
sync.WaitGroup. - Seul l'émetteur ferme un channel ;
for ranges'arrête à la fermeture, écrire dans un channel fermé panique. - Les deux pièges sont le deadlock (attente mutuelle) et la fuite de goroutine bloquée pour toujours.