Programmation asynchrone en Go : concepts de base et pratique
Go (Golang) a été conçu dès le départ comme un langage pour travailler efficacement avec des tâches concurrentes. Contrairement à de nombreux autres langages où l'asynchronisme est obtenu via des callbacks ou des bibliothèques complexes, Go propose des outils simples et élégants : les goroutines, les canaux et l'opérateur select. Dans ce guide, nous allons voir comment fonctionne la programmation asynchrone en Go et montrer des exemples pratiques.
1. Goroutines : des threads d'exécution légers
Une goroutine est un thread d'exécution léger géré par le runtime de Go. Contrairement aux threads système, les goroutines ont une surcharge minimale. Vous pouvez lancer des milliers de goroutines sans impact significatif sur les performances.
Pour lancer une goroutine, on utilise le mot-clé go avant un appel de fonction :
package main
import ( "fmt" "time")
func printMessage(msg string) { for i := 0; i < 5; i++ { fmt.Println(msg) time.Sleep(100 * time.Millisecond) }}
func main() { // Lancement d'une goroutine go printMessage("Affichage asynchrone") // Le thread principal continue son travail printMessage("Affichage synchrone") // On donne du temps à la goroutine pour se terminer time.Sleep(1 * time.Second)}Il est important de se rappeler : la fonction principale main est aussi une goroutine. Si elle se termine, toutes les autres goroutines sont détruites. C'est pourquoi on utilise souvent sync.WaitGroup ou des canaux pour la synchronisation.
2. Canaux : transfert sécurisé de données entre goroutines
Les canaux sont des "tuyaux" typés à travers lesquels les goroutines peuvent échanger des données. Le principe de base : "Ne communiquez pas via la mémoire partagée ; partagez la mémoire via la communication".
Création et utilisation d'un canal :
package main
import "fmt"
func main() { // Création d'un canal non bufferisé pour int ch := make(chan int) // Lancement d'une goroutine qui envoie des données go func() { for i := 0; i < 5; i++ { ch <- i * 2 } close(ch) // Fermeture du canal après l'envoi de toutes les données }() // Lecture des données depuis le canal for value := range ch { fmt.Println("Reçu :", value) }}Canaux bufferisés permettent d'envoyer des données sans réception immédiate :
ch := make(chan string, 3) // Buffer de 3 élémentsch <- "A"ch <- "B"ch <- "C"// On peut maintenant lirefmt.Println(<-ch) // Afmt.Println(<-ch) // B3. Opérateur select : travailler avec plusieurs canaux
L'opérateur select permet à une goroutine d'attendre simultanément plusieurs opérations sur des canaux. Il se bloque jusqu'à ce qu'un de ses cas soit prêt à être exécuté.
Exemple avec timeout et traitement de plusieurs sources de données :
package main
import ( "fmt" "time")
func worker(id int, ch chan string) { time.Sleep(time.Duration(id) * time.Second) ch <- fmt.Sprintf("Résultat du worker %d", id)}
func main() { ch1 := make(chan string) ch2 := make(chan string) go worker(2, ch1) // Se termine dans 2 sec go worker(3, ch2) // Se termine dans 3 sec select { case msg1 := <-ch1: fmt.Println("Premier canal :", msg1) case msg2 := <-ch2: fmt.Println("Deuxième canal :", msg2) case <-time.After(1 * time.Second): fmt.Println("Timeout ! Aucun worker n'a terminé à temps") } // Fermeture des canaux pour éviter les fuites close(ch1) close(ch2)}L'opérateur select est idéal pour implémenter des timeouts, des opérations non bloquantes et le pattern "fan-in" (fusion de plusieurs canaux en un seul).
4. Patterns de concurrence en Go
Considérez