Go Goroutines et Channels : Guide Pratique de Concurrence

Backend2026-09-30TryQuickToolBox

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

Bonnes Pratiques

Comparaison : Channels vs. Mutex

AspectChannelsMutex
Cas d'usageTransmission de données, coordinationProtection de l'état partagé
ComplexitéPlus élevée pour des tâches simplesPlus faible pour des tâches simples
PerformanceOverhead de synchronisationGénéralement plus rapide pour de courtes sections critiques
RisqueInterblocages, fuitesInterblocages 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.