backend / go
Channels
Explication
Ce que vous allez apprendre
- Créer et utiliser un channel non bufferisé pour synchroniser deux goroutines
- Comprendre la différence de comportement entre un channel bufferisé et non bufferisé
- Fermer un channel correctement et savoir qui a le droit de le faire
- Utiliser des channels directionnels pour sécuriser une signature de fonction
- Construire un pipeline simple en chaînant plusieurs channels
Dans quel contexte ?
Une goroutine télécharge un gros fichier en arrière-plan pendant que le reste du programme continue de tourner. Comment le code principal sait-il exactement quand ce téléchargement est terminé, sans deviner un délai avec time.Sleep ? C'est précisément le problème que les channels résolvent : ils permettent à une goroutine de signaler explicitement un événement, ou de transmettre une valeur, à une autre goroutine.
D'abord, la philosophie de Go sur le partage de données
Une citation résume l'esprit du langage sur ce sujet : "ne communique pas en partageant de la mémoire, partage de la mémoire en communiquant." Plutôt que de protéger une variable partagée avec des verrous (vu dans une prochaine leçon), les channels permettent de faire transiter une valeur d'une goroutine à une autre de façon sûre par construction.
ch := make(chan string) crée un channel non bufferisé : l'envoi (ch <- valeur) bloque tant qu'aucune autre goroutine n'est prête à recevoir (<-ch). C'est un vrai rendez-vous synchronisé entre les deux parties.
Une fois ce mécanisme de base compris, une variante devient utile
Un channel bufferisé, créé avec make(chan int, 3), accepte jusqu'à 3 valeurs sans bloquer l'envoi immédiatement. Ce n'est qu'une fois le buffer plein que l'envoi suivant se met en attente.
| Type de channel | Comportement de l'envoi | Cas d'usage typique |
|---|---|---|
| Non bufferisé | Bloque jusqu'à réception | Synchronisation stricte entre deux goroutines |
| Bufferisé (capacité N) | Bloque seulement si N valeurs en attente | Découpler producteur et consommateur |
Fermé (close) | Panique si on tente d'envoyer | Signaler la fin d'un flux au récepteur |
Prérequis
Il faut avoir compris les goroutines et sync.WaitGroup (leçon précédente) : les channels servent souvent à coordonner exactement ce que les goroutines exécutent en parallèle.
Il reste une règle stricte à respecter avec close
Fermer un channel (close(ch)) signale qu'aucune valeur supplémentaire n'y sera jamais envoyée. Un for range sur un channel s'arrête automatiquement quand celui-ci est fermé et vidé — c'est ce qui rend close indispensable pour toute boucle range sur un channel de longueur inconnue à l'avance.
Piège dangereux
Seul l'émetteur doit fermer un channel, jamais le récepteur. Envoyer une valeur sur un channel déjà fermé provoque un panic immédiat, et fermer un channel déjà fermé aussi. En cas de doute sur qui doit fermer, c'est presque toujours celui qui produit les données (le "producteur"), jamais celui qui les consomme.
Ensuite, comment documenter et sécuriser l'usage d'un channel
Un channel directionnel restreint son usage dans une signature de fonction : chan<- int n'autorise que l'envoi, <-chan int n'autorise que la réception. Cette restriction est vérifiée à la compilation et documente immédiatement l'intention d'une fonction qui prend un channel en paramètre.
Bonne pratique
Utilise systématiquement des channels directionnels dans les signatures de fonctions qui manipulent des channels : cela évite qu'une fonction censée uniquement consommer un flux ne puisse accidentellement y écrire, et inversement.
Un pipeline enchaîne plusieurs étapes via des channels successifs (générer, puis transformer, puis consommer), un pattern extrêmement idiomatique en Go. Maintenant que tu sais faire communiquer deux goroutines via un seul channel, la prochaine leçon montre comment en surveiller plusieurs à la fois avec select.
Commandes & code
Channels
Les channels permettent aux goroutines de communiquer et de se synchroniser sans locks explicites.
package main
import "fmt"
func main() {
// Channel non bufferise : l'envoi bloque jusqu'a reception (rendez-vous)
ch := make(chan string)
go func() {
ch <- "message depuis la goroutine" // envoi
}()
message := <-ch // reception (bloquant)
fmt.Println(message)
// Channel bufferise : l'envoi ne bloque que si le buffer est plein
buffer := make(chan int, 3)
buffer <- 1
buffer <- 2
buffer <- 3
// buffer <- 4 // bloquerait ici : capacite depassee
fmt.Println(<-buffer, <-buffer, <-buffer)
// Fermer un channel : signale qu'aucune autre valeur ne viendra
nombres := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
nombres <- i
}
close(nombres) // OBLIGATOIRE pour que range sache s'arreter
}()
for n := range nombres { // range sur un channel s'arrete a la fermeture
fmt.Println("recu:", n)
}
// Detecter la fermeture explicitement avec le "comma ok"
ch2 := make(chan int, 1)
ch2 <- 42
close(ch2)
v, ouvert := <-ch2
fmt.Println(v, ouvert) // 42 true : derniere valeur encore lisible
v, ouvert = <-ch2
fmt.Println(v, ouvert) // 0 false : channel ferme et vide
// Channel directionnel : restreint l'usage en signature (documentation + securite)
producteur := func(sortie chan<- int, n int) { // sortie: envoi seulement
for i := 0; i < n; i++ {
sortie <- i
}
close(sortie)
}
consommateur := func(entree <-chan int) int { // entree: reception seulement
total := 0
for v := range entree {
total += v
}
return total
}
c := make(chan int)
go producteur(c, 5)
fmt.Println(consommateur(c))
// Pipeline : chainer des etapes via des channels
generer := func(n int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 1; i <= n; i++ {
out <- i
}
}()
return out
}
carre := func(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in {
out <- v * v
}
}()
return out
}
for v := range carre(generer(5)) {
fmt.Println("carre:", v)
}
}Résumé
- Channel non bufferisé = synchronisation stricte ; bufferisé = découplage jusqu'à N éléments.
close(ch)signale la fin du flux ; seul l'émetteur doit fermer, jamais le récepteur.- Channels directionnels (
chan<-,<-chan) documentent et sécurisent les signatures. - Les pipelines (channels chaînés) sont le pattern Go idiomatique pour le traitement en flux.
Exercices pratiques
Mission : un pipeline d'import de commandes qui se bloque au démarrage
Objectif : Diagnostiquer un deadlock lié à un channel non bufferisé mal utilisé, puis sécuriser un pipeline producteur-consommateur avec des channels directionnels.
Contexte
Un pipeline d'import lit des commandes depuis un fichier CSV, les envoie sur un channel commandes := make(chan Commande) vers une goroutine de validation. Le code fonctionne parfaitement en test avec un mock qui lance la goroutine en premier, mais en production le programme se fige complètement dès le premier envoi, sans jamais planter ni afficher d'erreur.