Go Goroutines et Channels : Guide Pratique de Concurrence
Vous avez probablement déjà écrit du code Go qui fonctionne bien jusqu'à ce qu'il ne fonctionne plus—peut-être une course de données qui fait planter votre service sous charge, ou un interblocage qui gèle votre API. La concurrence est l'un des plus grands atouts de Go, mais c'est aussi là que beaucoup de développeurs se bloquent. Les goroutines et les channels sont les briques de base, mais les utiliser correctement nécessite de comprendre leur comportement et les pièges courants.
Ce guide présente des patterns pratiques pour utiliser les goroutines et les channels, avec des exemples de code et des conseils que vous pouvez appliquer immédiatement.
Que sont les Goroutines ?
Une goroutine est un thread léger géré par le runtime Go. Vous en créez une en préfixant un appel de fonction avec go :
go doSomething()
Les goroutines démarrent avec une petite pile (quelques kilo-octets) qui grandit selon les besoins. Cela les rend peu coûteuses à créer par milliers, voire millions. Le scheduler Go multiplexe les goroutines sur les threads du système d'exploitation, vous ne gérez donc pas les threads directement.
Mais créer une goroutine sans coordination est une recette pour le chaos. Vous avez besoin d'un moyen de les attendre, de transmettre des données ou de les annuler—c'est là que les channels entrent en jeu.
Channels : Communication Entre Goroutines
Les channels sont des conduits typés pour envoyer et recevoir des valeurs. Ils assurent la synchronisation : un envoi bloque jusqu'à ce qu'un récepteur soit prêt, et une réception bloque jusqu'à ce qu'une valeur soit disponible. Cela rend les channels idéaux pour coordonner les goroutines.
Créez un channel avec make(chan T) :
ch := make(chan int)
go func() {
ch <- 42 // envoi
}()
value := <-ch // réception
Par défaut, les channels ne sont pas bufferisés, ce qui signifie que les envois et réceptions se font en synchronisation stricte. Les channels bufferisés permettent un nombre limité d'envois sans récepteur :
ch := make(chan int, 3) // taille du buffer 3
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 bloquerait jusqu'à ce que quelqu'un reçoive
Les channels bufferisés peuvent lisser les pics mais n'éliminent pas le besoin de coordination.
Patterns de Concurrence Courants
1. Pool de Workers
Répartissez les tâches sur un nombre fixe de workers pour limiter l'utilisation des ressources :
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs)
for r := 1; r <= 5; r++ {
<-results
}
}
Ce pattern est excellent pour les tâches liées au CPU ou aux E/S où vous voulez contrôler le parallélisme.
2. Fan-Out, Fan-In
Fan-out signifie que plusieurs goroutines lisent depuis le même channel. Fan-in signifie fusionner les résultats de plusieurs channels en un seul :
func fanIn(chs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chs))
for _, ch := range chs {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Utilisez ceci lorsque vous avez plusieurs producteurs et voulez un seul flux de résultats.
3. Select pour le Multiplexage
L'instruction select permet à une goroutine d'attendre plusieurs opérations sur channel :
select {
case msg := <-ch1:
fmt.Println("received from ch1:", msg)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
Utilisez select avec un cas default pour les opérations non bloquantes, ou avec un timeout pour éviter d'attendre indéfiniment.
Pièges et Comment les Éviter
- Fuites de goroutines : Une goroutine bloquée sur un envoi/réception de channel pour toujours ne sera jamais collectée par le garbage collector. Assurez-vous toujours que les channels sont fermés ou utilisez l'annulation par context.
- Interblocages : Les dépendances circulaires entre channels provoquent des interblocages. Utilisez des timeouts ou concevez sans cycles.
- Courses de données : Accéder à la mémoire partagée sans synchronisation conduit à des races. Utilisez des channels ou des mutex pour protéger l'état partagé.
- Fermeture des channels : Seul l'expéditeur doit fermer un channel. Fermer depuis le récepteur peut provoquer des panics. De plus, envoyer sur un channel fermé provoque un panic.
Bonnes Pratiques
- Préférez les channels pour la coordination, les mutex pour l'état : Utilisez les channels pour transmettre des données et signaler entre goroutines ; utilisez
sync.Mutexpour protéger un état partagé simple. - Utilisez context pour l'annulation : Propagez
context.Contextpour permettre un arrêt gracieux et des timeouts. - Limitez la concurrence : Utilisez des pools de workers ou des sémaphores pour éviter de submerger les ressources.
- Testez avec le détecteur de races : Exécutez
go test -racepour détecter les races de données tôt. - Restez simple : Ne sur-ingénieriez pas. Souvent un mutex est plus simple et plus rapide qu'une chorégraphie complexe de channels.
Comparaison : Channels vs. Mutex
| Aspect | Channels | Mutex |
|---|---|---|
| Cas d'usage | Transmission de données, coordination | Protection de l'état partagé |
| Complexité | Plus élevée pour des tâches simples | Plus faible pour des tâches simples |
| Performance | Overhead de synchronisation | Généralement plus rapide pour de courtes sections critiques |
| Risque | Interblocages, fuites | Interblocages si pas prudent |
FAQ
Quand dois-je utiliser un channel bufferisé ?
Utilisez un channel bufferisé lorsque vous voulez découpler les expéditeurs et les récepteurs pour gérer les pics, ou lorsque vous connaissez le nombre exact d'envois et voulez éviter le blocage. Cependant, les channels non bufferisés offrent de meilleures garanties de synchronisation et sont souvent plus faciles à raisonner.
Comment éviter les fuites de goroutines ?
Assurez-vous que chaque goroutine a un chemin de sortie clair. Utilisez context.Context pour signaler l'annulation, fermez les channels quand plus aucune valeur ne sera envoyée, et évitez de bloquer indéfiniment sur des opérations de channel sans timeout ni cas default.
Puis-je utiliser des channels pour tout ?
Non. Bien que les channels soient puissants, ils ajoutent de l'overhead et de la complexité. Pour un état partagé simple, un mutex est souvent plus direct et efficace. Le proverbe Go dit : « Ne communiquez pas en partageant la mémoire ; partagez la mémoire en communiquant », mais cela ne signifie pas que les channels sont toujours la réponse.
Si vous déboguez des problèmes de concurrence ou analysez les logs de vos services Go, l'Analyseur de Logs Nginx peut vous aider à repérer rapidement des patterns et des erreurs.