# Concurrency Go : Goroutines et Channels - Guide Complet > Maîtrisez la concurrence en Go avec goroutines et channels. Patterns avancés, synchronisation, select, et bonnes pratiques avec exemples de code détaillés. - Published: 2026-02-21 - Updated: 2026-03-31 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, channels - Reading time: 14 min --- La concurrence représente l'un des points forts de Go. Contrairement à d'autres langages où le multithreading reste complexe, Go propose un modèle élégant basé sur les goroutines et channels qui simplifie considérablement le développement d'applications concurrentes. > **Philosophie Go** > > "Ne communiquez pas en partageant la mémoire, partagez la mémoire en communiquant." Ce principe fondamental guide toute la conception de la concurrence en Go. ## Comprendre les goroutines Les goroutines sont des threads légers gérés par le runtime Go. Elles consomment environ 2 Ko de stack (contre plusieurs Mo pour un thread OS) et permettent d'exécuter des milliers de tâches concurrentes sans surcharge système. Le lancement d'une goroutine s'effectue simplement avec le mot-clé `go` devant un appel de fonction. Le runtime se charge de la planification et de la distribution sur les threads disponibles. ```go // goroutines_basic.go package main import ( "fmt" "time" ) // fetchData simule une requête réseau func fetchData(id int) { // Simule un délai réseau time.Sleep(100 * time.Millisecond) fmt.Printf("Données %d récupérées\n", id) } func main() { // Lancement séquentiel - 500ms total start := time.Now() for i := 1; i <= 5; i++ { fetchData(i) } fmt.Printf("Séquentiel: %v\n", time.Since(start)) // Lancement concurrent - ~100ms total start = time.Now() for i := 1; i <= 5; i++ { go fetchData(i) // Exécution en goroutine } time.Sleep(150 * time.Millisecond) // Attend la fin fmt.Printf("Concurrent: %v\n", time.Since(start)) } ``` L'exécution concurrente réduit le temps total de 500ms à environ 100ms. Cependant, utiliser `time.Sleep` pour synchroniser les goroutines n'est pas une bonne pratique. Les channels offrent une solution élégante. ## Channels : communication entre goroutines Un channel est un conduit typé permettant l'envoi et la réception de valeurs entre goroutines. Les channels assurent la synchronisation : une goroutine qui envoie attend qu'une autre reçoive, et inversement. La création d'un channel utilise la fonction `make`. L'opérateur `<-` permet d'envoyer et recevoir des données selon son placement par rapport au channel. ```go // channels_basic.go package main import "fmt" // worker effectue un calcul et renvoie le résultat via channel func worker(id int, jobs <-chan int, results chan<- int) { // Reçoit les jobs jusqu'à fermeture du channel for job := range jobs { result := job * 2 // Traitement results <- result // Envoi du résultat } } func main() { // Création des channels jobs := make(chan int, 10) // Channel bufferisé results := make(chan int, 10) // Démarrage de 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // Envoi de 5 jobs for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // Signale la fin des jobs // Collecte des résultats for r := 1; r <= 5; r++ { result := <-results fmt.Printf("Résultat: %d\n", result) } } ``` Les channels directionnels (`<-chan` pour réception, `chan<-` pour envoi) renforcent la sécurité du code en limitant les opérations possibles. ## Channels bufferisés vs non-bufferisés La distinction entre ces deux types de channels influence directement le comportement de synchronisation des goroutines. Les channels non-bufferisés bloquent l'expéditeur jusqu'à ce qu'un récepteur soit prêt. Les channels bufferisés permettent d'envoyer jusqu'à N valeurs sans blocage, où N représente la capacité du buffer. ```go // buffered_channels.go package main import "fmt" func main() { // Channel non-bufferisé - synchronisation stricte unbuffered := make(chan string) go func() { unbuffered <- "message" // Bloque jusqu'à réception }() msg := <-unbuffered // Débloque l'envoi fmt.Println(msg) // Channel bufferisé - capacité de 2 buffered := make(chan int, 2) // Ces envois ne bloquent pas buffered <- 1 buffered <- 2 // buffered <- 3 // Bloquerait car buffer plein fmt.Println(<-buffered) // 1 fmt.Println(<-buffered) // 2 // Vérification de la capacité fmt.Printf("Longueur: %d, Capacité: %d\n", len(buffered), cap(buffered)) } ``` Les channels bufferisés s'utilisent pour découpler producteurs et consommateurs, tandis que les non-bufferisés garantissent une synchronisation point à point. > **Attention aux deadlocks** > > Un deadlock survient quand toutes les goroutines sont bloquées en attente. Le runtime Go le détecte et termine le programme avec un message d'erreur explicite. ## Select : multiplexage de channels L'instruction `select` permet d'attendre sur plusieurs opérations de channel simultanément. Elle s'apparente à un switch pour les communications concurrentes. Cette construction est essentielle pour gérer des timeouts, annulations et communications multiples sans bloquer indéfiniment sur un seul channel. ```go // select_example.go package main import ( "fmt" "time" ) // fetchAPI simule un appel API avec délai variable func fetchAPI(name string, delay time.Duration, ch chan<- string) { time.Sleep(delay) ch <- fmt.Sprintf("%s: données reçues", name) } func main() { api1 := make(chan string) api2 := make(chan string) // Lancement de deux appels API en parallèle go fetchAPI("API-1", 100*time.Millisecond, api1) go fetchAPI("API-2", 200*time.Millisecond, api2) // Timeout global de 150ms timeout := time.After(150 * time.Millisecond) // Collecte des résultats avec timeout for i := 0; i < 2; i++ { select { case result := <-api1: fmt.Println(result) case result := <-api2: fmt.Println(result) case <-timeout: fmt.Println("Timeout - opération annulée") return } } } ``` Le `select` choisit le premier channel prêt. Si plusieurs sont prêts, le choix est pseudo-aléatoire pour éviter la famine. ## Pattern Worker Pool Le pattern worker pool distribue des tâches entre plusieurs workers, limitant la concurrence et optimisant l'utilisation des ressources. Ce pattern s'avère indispensable pour traiter de grandes quantités de données. L'implémentation repose sur un channel de jobs partagé entre workers et un channel de résultats pour la collecte. ```go // worker_pool.go package main import ( "fmt" "sync" "time" ) // Task représente une unité de travail type Task struct { ID int Data string } // Result contient le résultat du traitement type Result struct { TaskID int Output string } // worker traite les tâches reçues func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() for task := range tasks { // Simulation du traitement time.Sleep(50 * time.Millisecond) results <- Result{ TaskID: task.ID, Output: fmt.Sprintf("Worker %d a traité: %s", id, task.Data), } } } func main() { const numWorkers = 3 const numTasks = 10 tasks := make(chan Task, numTasks) results := make(chan Result, numTasks) var wg sync.WaitGroup // Démarrage des workers for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, tasks, results, &wg) } // Envoi des tâches for i := 1; i <= numTasks; i++ { tasks <- Task{ID: i, Data: fmt.Sprintf("tâche-%d", i)} } close(tasks) // Fermeture du channel results après fin des workers go func() { wg.Wait() close(results) }() // Collecte des résultats for result := range results { fmt.Printf("Tâche %d: %s\n", result.TaskID, result.Output) } } ``` Le `sync.WaitGroup` coordonne l'attente de la fin de tous les workers avant de fermer le channel de résultats. ## Pattern Fan-Out/Fan-In Ce pattern distribue le travail entre plusieurs goroutines (fan-out) puis agrège les résultats (fan-in). Il maximise le parallélisme tout en simplifiant la collecte des résultats. ```go // fan_out_fan_in.go package main import ( "fmt" "sync" ) // generate produit des nombres sur un channel func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square calcule le carré des nombres reçus func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // merge combine plusieurs channels en un seul (fan-in) func merge(channels ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup // Fonction de sortie pour chaque channel output := func(c <-chan int) { defer wg.Done() for n := range c { out <- n } } // Lancement d'une goroutine par channel wg.Add(len(channels)) for _, c := range channels { go output(c) } // Fermeture après fin de toutes les goroutines go func() { wg.Wait() close(out) }() return out } func main() { // Génération des données numbers := generate(1, 2, 3, 4, 5, 6, 7, 8) // Fan-out: distribution sur 3 workers sq1 := square(numbers) sq2 := square(numbers) sq3 := square(numbers) // Fan-in: agrégation des résultats for result := range merge(sq1, sq2, sq3) { fmt.Println(result) } } ``` Ce pattern brille pour les opérations CPU-bound distribuables et les pipelines de traitement de données. ## Context pour l'annulation et les deadlines Le package `context` standardise la gestion des annulations, deadlines et valeurs à travers les goroutines. Toute goroutine longue durée devrait accepter un context comme premier paramètre. ```go // context_example.go package main import ( "context" "fmt" "time" ) // fetchWithContext simule une requête annulable func fetchWithContext(ctx context.Context, url string) (string, error) { // Simule une opération longue select { case <-time.After(2 * time.Second): return fmt.Sprintf("Données de %s", url), nil case <-ctx.Done(): return "", ctx.Err() // context.Canceled ou context.DeadlineExceeded } } func main() { // Context avec timeout de 500ms ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // Libère les ressources result, err := fetchWithContext(ctx, "https://api.example.com") if err != nil { fmt.Printf("Erreur: %v\n", err) return } fmt.Println(result) // Context avec annulation manuelle ctx2, cancel2 := context.WithCancel(context.Background()) go func() { time.Sleep(100 * time.Millisecond) cancel2() // Annulation explicite }() result, err = fetchWithContext(ctx2, "https://api2.example.com") if err != nil { fmt.Printf("Requête annulée: %v\n", err) } } ``` > **Bonne pratique** > > Toujours appeler `defer cancel()` immédiatement après création du context pour éviter les fuites de ressources. ## Synchronisation avec sync.Mutex Bien que les channels soient privilégiés pour la communication, le package `sync` reste nécessaire pour protéger l'accès concurrent aux structures de données partagées. ```go // mutex_example.go package main import ( "fmt" "sync" ) // SafeCounter est un compteur thread-safe type SafeCounter struct { mu sync.Mutex value map[string]int } // Increment incrémente la valeur pour une clé donnée func (c *SafeCounter) Increment(key string) { c.mu.Lock() // Verrouillage exclusif defer c.mu.Unlock() // Déverrouillage garanti c.value[key]++ } // Value retourne la valeur actuelle func (c *SafeCounter) Value(key string) int { c.mu.Lock() defer c.mu.Unlock() return c.value[key] } func main() { counter := SafeCounter{value: make(map[string]int)} var wg sync.WaitGroup // 1000 incrémentations concurrentes for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment("visits") }() } wg.Wait() fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000 } ``` Le `sync.RWMutex` optimise les lectures concurrentes avec `RLock()`/`RUnlock()` pour les opérations de lecture uniquement. ## Erreurs courantes et solutions La concurrence en Go présente des pièges classiques. Voici les erreurs les plus fréquentes et comment les éviter. ```go // common_mistakes.go package main import ( "fmt" "sync" ) func main() { // ERREUR: Capture de variable de boucle // Toutes les goroutines afficheraient la même valeur for i := 0; i < 3; i++ { go func() { fmt.Println(i) // Capture par référence - BUG }() } // SOLUTION: Passer la valeur en paramètre var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Println(n) // Copie locale - CORRECT }(i) } wg.Wait() // ERREUR: Envoi sur channel nil var ch chan int // ch <- 1 // Bloque indéfiniment // SOLUTION: Toujours initialiser avec make ch = make(chan int, 1) ch <- 1 fmt.Println(<-ch) // ERREUR: Envoi sur channel fermé done := make(chan bool) close(done) // done <- true // Panique! // SOLUTION: Vérifier avant envoi ou utiliser sync.Once select { case done <- true: fmt.Println("Envoyé") default: fmt.Println("Channel fermé ou plein") } } ``` La détection des data races s'effectue avec le flag `-race` lors de la compilation ou des tests : `go test -race ./...`. ## Conclusion La maîtrise de la concurrence en Go repose sur quelques concepts clés qui, bien compris, permettent de construire des applications hautement performantes. **Points essentiels à retenir :** ✅ Les goroutines sont légères et bon marché - en créer des milliers reste acceptable ✅ Les channels synchronisent et transfèrent les données entre goroutines ✅ Le `select` gère les communications multiples et les timeouts ✅ Le pattern worker pool limite la concurrence et optimise les ressources ✅ Le package `context` standardise annulation et deadlines ✅ Les mutex protègent les données partagées quand les channels ne suffisent pas ✅ Le flag `-race` détecte les data races lors des tests La philosophie "Share memory by communicating" guide vers des designs plus sûrs et maintenables que le multithreading traditionnel avec locks. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/go/concurrency-go-goroutines-channels