Introduction : Pourquoi la programmation asynchrone en Go ?
Go (Golang) a été conçu dès le départ pour une gestion efficace de la concurrence. Contrairement à de nombreux langages où l'asynchronisme est implémenté via des callbacks complexes ou des promesses, Go propose un modèle élégant basé sur les goroutines et les channels. Ce modèle permet d'écrire du code asynchrone qui se lit comme du code synchrone, mais qui s'exécute en parallèle, en utilisant au maximum les ressources des processeurs multicœurs.
Dans ce guide, nous allons aborder tous les concepts clés de la programmation asynchrone en Go : de la création de goroutines aux modèles de synchronisation complexes utilisant les channels et les primitives du package sync. Ce contenu sera utile aussi bien aux débutants qui découvrent Go qu'aux développeurs expérimentés souhaitant systématiser leurs connaissances.
1. Goroutines : Des threads légers
Une goroutine est un thread d'exécution léger géré par le runtime de Go. Contrairement aux threads du système d'exploitation, les goroutines ont une pile minuscule (commençant à quelques Ko) qui peut croître et rétrécir dynamiquement. Cela permet d'exécuter des dizaines de milliers de goroutines simultanément sans consommation de mémoire significative.
Lancer une goroutine
Pour exécuter une fonction en tant que goroutine, il suffit d'ajouter le mot-clé go avant l'appel de la fonction :
package main
import ( "fmt" "time")
func printNumbers() { for i := 1; i <= 5; i++ { fmt.Println(i) time.Sleep(100 * time.Millisecond) }}
func main() { // Exécute la fonction en tant que goroutine go printNumbers()
// La goroutine principale continue son travail fmt.Println("La goroutine principale fonctionne")
// Donne du temps pour l'exécution de la goroutine fille time.Sleep(1 * time.Second) fmt.Println("Programme terminé")}Important : Le programme se termine lorsque la fonction main() se termine. Si la goroutine principale termine son travail plus tôt, toutes les goroutines filles seront arrêtées de force. C'est pourquoi dans l'exemple nous utilisons time.Sleep pour attendre — en pratique, on utilise les channels ou sync.WaitGroup pour la synchronisation.
Fonctions anonymes dans les goroutines
Les goroutines sont souvent créées à l'aide de fonctions anonymes :
go func() { fmt.Println("Exécuté dans une goroutine")}()2. Channels : Communication entre goroutines
Les channels sont des "tuyaux" typés à travers lesquels les goroutines peuvent envoyer et recevoir des valeurs. C'est le mécanisme principal de synchronisation et d'échange de données en Go. Les channels assurent un accès sécurisé aux données sans utiliser de verrous explicites (mutex).
Création et utilisation d'un channel
Un channel est créé avec la fonction make(). L'opérateur <- est utilisé pour envoyer et recevoir des données :
package main
import "fmt"
func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // envoie le résultat dans le channel}
func main() { s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
x, y := <-c, <-c // reçoit les données du channel
fmt.Println(x, y, x+y)}Dans cet exemple, nous lançons deux goroutines, chacune calcule la somme de la moitié d'une slice. Les résultats sont envoyés dans le channel, et la goroutine principale les reçoit et affiche la somme totale.
Channels bufferisés
Par défaut, les channels ne sont pas bufferisés — l'envoi est bloqué jusqu'à ce que le récepteur soit prêt à accepter la valeur. Les channels bufferisés ont une file d'attente de taille définie :
ch := make(chan int, 2) // channel avec un buffer de 2 élémentsch <- 1ch <- 2// ch <- 3 // erreur : deadlock, buffer pleinfmt.Println(<-ch) // 1fmt.Println(<-ch) // 2