backend / go
Select et patterns de concurrence
Explication
Ce que vous allez apprendre
- Utiliser
selectpour attendre sur plusieurs channels simultanément - Implémenter un timeout robuste avec
selectettime.After - Rendre un
selectnon bloquant grâce àdefault - Construire un pattern fan-out / fan-in avec un pool de workers
- Annuler proprement une opération en cours avec
context
Dans quel contexte ?
Un service appelle une API externe qui peut parfois ne jamais répondre à cause d'un problème réseau côté fournisseur. Sans mécanisme de timeout, la requête resterait bloquée indéfiniment, gelant potentiellement toute une chaîne de traitement derrière elle. select combiné à time.After est le pattern standard pour éviter exactement ce scénario en production.
D'abord, select généralise l'idée d'attendre sur un channel
Alors qu'une simple réception <-ch bloque sur un seul channel, select surveille plusieurs channels à la fois et exécute le bloc correspondant au premier qui devient prêt. Si plusieurs channels sont prêts simultanément, Go en choisit un au hasard, pour éviter tout biais de priorité involontaire.
Cette capacité à "écouter sur plusieurs sources à la fois" est la brique de base de presque tous les patterns de concurrence avancés du langage.
Une fois ce mécanisme acquis, le pattern de timeout devient naturel
time.After(duree) retourne un channel qui reçoit une valeur unique après le délai indiqué. En le plaçant comme un cas parmi d'autres dans un select, on obtient un timeout élégant : soit le résultat attendu arrive à temps, soit le délai s'écoule en premier et on abandonne l'attente.
| Pattern | Composants | Effet |
|---|---|---|
| Timeout | select + time.After | Abandonne l'attente après un délai |
| Non bloquant | select + default | Vérifie un channel sans jamais attendre |
| Fan-out / fan-in | N goroutines + 1 channel partagé | Paralléliser un traitement avec un parallélisme borné |
| Annulation | context.WithTimeout/WithCancel | Propager un arrêt à travers plusieurs goroutines |
Prérequis
Cette leçon combine directement goroutines et channels (deux leçons précédentes) : une bonne maîtrise des deux est indispensable pour suivre les exemples de worker pool.
Il reste un usage particulier de select à connaître : le mode non bloquant
Ajouter un cas default à un select change complètement son comportement : au lieu d'attendre qu'un channel soit prêt, il exécute immédiatement default si aucun channel ne l'est. C'est utile pour vérifier "y a-t-il un message en attente ?" sans jamais bloquer l'exécution du programme.
Piège fréquent
Un select sans default et sans aucun cas qui deviendra jamais prêt bloque indéfiniment ("deadlock"), et Go détecte ce cas au runtime avec un message explicite si c'est la seule goroutine active. Vérifie toujours qu'au moins un cas de ton select a une réelle chance de se réaliser.
Enfin, le pattern fan-out / fan-in structure le parallélisme
Plusieurs "workers" (goroutines identiques) consomment des tâches depuis un seul channel partagé, et envoient leurs résultats vers un channel commun. Ce pattern borne naturellement le parallélisme au nombre de workers lancés, évitant de saturer la machine avec un nombre incontrôlé de goroutines simultanées.
Bonne pratique
Pour annuler une opération à travers plusieurs goroutines de façon coordonnée, préfère toujours context.WithTimeout/WithCancel à un channel "done" fait maison : c'est l'idiome standard de toute la bibliothèque standard et de l'écosystème Go, et il sera approfondi dans une prochaine leçon.
Maintenant que tu sais orchestrer plusieurs channels, il te manque encore un outil pour protéger un état partagé quand les channels ne suffisent pas : place au package sync et à ses mutex.
Commandes & code
Select et patterns de concurrence
select attend sur plusieurs channels simultanément : la base des patterns de concurrence avancés.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(50 * time.Millisecond)
ch1 <- "resultat de ch1"
}()
go func() {
time.Sleep(20 * time.Millisecond)
ch2 <- "resultat de ch2"
}()
// select choisit le premier channel pret (ici ch2, plus rapide)
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
// Timeout avec select : pattern tres courant en production
resultat := make(chan string)
go func() {
time.Sleep(200 * time.Millisecond)
resultat <- "trop lent"
}()
select {
case r := <-resultat:
fmt.Println(r)
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout depasse")
}
// select non-bloquant avec default
messages := make(chan int, 1)
select {
case m := <-messages:
fmt.Println("message:", m)
default:
fmt.Println("aucun message disponible")
}
// Fan-out / fan-in : plusieurs workers consomment une seule source
jobs := make(chan int, 10)
resultats := make(chan int, 10)
for w := 1; w <= 3; w++ {
go worker(w, jobs, resultats)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for i := 0; i < 5; i++ {
fmt.Println("resultat worker:", <-resultats)
}
// Annulation coordonnee avec un channel "done"
ctx, annuler := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer annuler()
longueTache(ctx)
}
func worker(id int, jobs <-chan int, resultats chan<- int) {
for j := range jobs {
resultats <- j * j
}
}
func longueTache(ctx context.Context) {
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("tache terminee normalement")
case <-ctx.Done():
fmt.Println("tache annulee:", ctx.Err())
}
}Résumé
selectbloque jusqu'à ce qu'un des cas soit prêt, choisi aléatoirement en cas d'égalité.time.After+selectimplémente un timeout sans bibliothèque externe.defaultdans unselectle rend non-bloquant.- Fan-out (plusieurs workers) / fan-in (résultats agrégés) est le pattern de base du parallélisme en Go.
Exercices pratiques
Mission : un appel à un fournisseur externe qui gèle toute la chaîne
Objectif : Ajouter un timeout robuste sur un appel réseau externe et dimensionner un pool de workers, sans provoquer de deadlock ni de fuite de goroutines.
Contexte
Le service de paiement appelle une API d'un fournisseur externe connu pour parfois ne jamais répondre. Le code actuel fait directement resultat := <-appelerFournisseur(commande) sans aucune protection. Depuis un incident réseau chez ce fournisseur la semaine dernière, toute la chaîne de traitement des commandes s'est retrouvée figée pendant plus d'une heure.