Retour au cours

backend / go

Channels

Leçon 121 exercice

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 channelComportement de l'envoiCas d'usage typique
Non bufferiséBloque jusqu'à réceptionSynchronisation stricte entre deux goroutines
Bufferisé (capacité N)Bloque seulement si N valeurs en attenteDécoupler producteur et consommateur
Fermé (close)Panique si on tente d'envoyerSignaler 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.

go
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

1 disponible
1

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.

Résoudre l’exercice →