Contactez-nous

Goroutines : lancer des fonctions en parallèle (`go ...`)

Apprenez à utiliser les goroutines en Go pour lancer des fonctions de manière concurrente et non bloquante à l'aide du simple mot-clé `go`. Comprenez leur nature légère.

La magie de la concurrence légère : Les Goroutines

Le coeur de la puissance concurrente de Go réside dans un concept simple mais profond : la goroutine. Imaginez une goroutine comme une fonction capable de s'exécuter indépendamment et simultanément (ou de manière quasi-simultanée) avec d'autres fonctions de votre programme. C'est le mécanisme fondamental par lequel Go atteint la concurrence.

Ce qui rend les goroutines particulièrement attrayantes, c'est leur extrême légèreté. Contrairement aux threads systèmes traditionnels gérés par le système d'exploitation, les goroutines sont gérées par le runtime Go lui-même. Elles ont une empreinte mémoire initiale très faible (quelques kilo-octets) et le coût de leur création et de leur changement de contexte est bien moindre que celui des threads. Cela signifie que vous pouvez lancer des centaines de milliers, voire des millions, de goroutines dans une seule application Go sans épuiser les ressources système.

Cette efficacité ouvre la porte à des modèles de programmation où il est courant de lancer une goroutine pour chaque tâche concurrente, même pour des opérations relativement courtes, comme la gestion d'une requête réseau individuelle.

Lancer une Goroutine : Simplicité du mot-clé `go`

La syntaxe pour démarrer une nouvelle goroutine est d'une simplicité remarquable. Il suffit de préfixer l'appel à une fonction (ou une méthode) par le mot-clé `go`.

Syntaxe : `go nomDeLaFonction(arguments...)`

Lorsque Go rencontre cette instruction, il ne bloque pas. Il lance l'exécution de `nomDeLaFonction` dans une nouvelle goroutine et continue immédiatement l'exécution de la ligne de code suivante dans la goroutine actuelle.

Voyons un exemple simple :

package main

import (
    "fmt"
    "time"
)

// Une fonction simple que nous allons lancer en goroutine
func direBonjour() {
    fmt.Println("Bonjour depuis la goroutine !")
}

func main() {
    fmt.Println("Début de la fonction main.")

    // Lancement de direBonjour dans une nouvelle goroutine
    go direBonjour()

    // Le programme principal continue immédiatement
    fmt.Println("direBonjour lancée en arrière-plan.")

    // ATTENTION : Problème potentiel ici !
    // Si main se termine trop vite, la goroutine n'aura peut-être pas le temps de s'exécuter.
    // Nous ajoutons une pause ici UNIQUEMENT pour la démonstration.
    // CE N'EST PAS LA BONNE FACON DE SYNCHRONISER EN PRODUCTION !
    time.Sleep(100 * time.Millisecond) 

    fmt.Println("Fin de la fonction main.")
}
Dans cet exemple, `go direBonjour()` lance la fonction `direBonjour` dans une nouvelle goroutine. Le `main` continue son exécution et affiche "direBonjour lancée en arrière-plan.". Si nous n'avions pas ajouté le `time.Sleep`, il est très probable que `main` se termine avant que la goroutine `direBonjour` ait eu le temps d'être planifiée par le runtime Go et d'afficher son message.

Exécution Asynchrone et Non-Bloquante

Le point clé à retenir est que l'instruction `go` est non-bloquante. La goroutine appelante (dans notre exemple, celle exécutant `main`) ne s'arrête pas pour attendre que la nouvelle goroutine (`direBonjour`) ait fini son travail. Elle continue son propre chemin d'exécution immédiatement.

C'est ce comportement asynchrone qui permet la concurrence. Plusieurs goroutines peuvent ainsi progresser indépendamment les unes des autres. Le runtime Go se charge de multiplexer ces goroutines sur un nombre plus limité de threads systèmes, gérant leur planification et leur exécution de manière efficace.

Le résultat de l'exemple précédent (avec le `Sleep`) illustre cela :

Début de la fonction main.
direBonjour lancée en arrière-plan.
Bonjour depuis la goroutine !
Fin de la fonction main.
(L'ordre exact entre "Bonjour depuis la goroutine !" et "Fin de la fonction main." peut varier légèrement, mais "direBonjour lancée en arrière-plan." apparaîtra toujours avant "Bonjour depuis la goroutine !")

Le défi de la synchronisation : Quand `main` se termine

Comme mentionné dans l'exemple, un aspect crucial à comprendre est que lorsque la goroutine principale (celle qui exécute `main`) se termine, l'ensemble du programme s'arrête, et ce, même si d'autres goroutines sont encore en cours d'exécution. Elles sont simplement interrompues.

C'est pourquoi l'utilisation de `time.Sleep` dans notre exemple est une mauvaise pratique pour la synchronisation réelle. Elle ne fait que masquer le problème en donnant arbitrairement du temps à l'autre goroutine. Si la tâche de la goroutine prenait plus de temps que le `Sleep`, elle serait quand même interrompue.

La nécessité d'attendre que les goroutines terminent leur travail avant de quitter `main` (ou avant de continuer une autre étape qui dépend de leur résultat) introduit le besoin de mécanismes de synchronisation explicites. Nous aborderons ces mécanismes (comme `sync.WaitGroup` et les canaux) très prochainement. Pour l'instant, retenez simplement que lancer une goroutine ne garantit pas qu'elle finira son travail si le programme principal se termine avant elle.

Goroutines et Fonctions Anonymes (Closures)

Il est très courant de lancer des goroutines en utilisant des fonctions anonymes (aussi appelées fonctions littérales ou closures). Cela permet de définir le code à exécuter de manière concurrente directement là où la goroutine est lancée.

Ces fonctions anonymes peuvent capturer des variables de leur portée environnante (c'est le principe des closures).

package main

import (
    "fmt"
    "time"
)

func main() {
    message := "Message depuis une closure !"
    iteration := 1

    // Lance une fonction anonyme en goroutine
    go func(msg string, i int) { 
        // Cette fonction a ses propres copies des arguments msg et i
        fmt.Printf("Goroutine %d dit: %s\n", i, msg)
    }(message, iteration) // Passe les variables comme arguments

    iteration = 2
    message = "Autre message" // Changer la variable après le lancement n'affecte pas la goroutine précédente
                              // car elle a reçu une copie via les paramètres.

    // Autre exemple capturant directement (attention aux modifications concurrentes potentielles)
    info := "Info capturée"
    go func() {
        // Cette goroutine capture 'info' directement depuis la portée de main
        time.Sleep(50 * time.Millisecond) // Laisse le temps à main de continuer
        fmt.Println("Goroutine capturant dit:", info)
    }()
    
    // Si 'info' était modifié ici avant que la goroutine ne lise,
    // la goroutine verrait la nouvelle valeur.

    fmt.Println("Goroutines lancées.")
    time.Sleep(200 * time.Millisecond) // Mauvaise synchro (pour démo)
    fmt.Println("Fin de main.")
}
L'utilisation de closures avec des goroutines est puissante, mais demande de la prudence, notamment lorsque les variables capturées peuvent être modifiées par la goroutine appelante après le lancement de la goroutine concurrente (surtout dans les boucles, un piège classique). Passer les valeurs nécessaires comme arguments à la fonction anonyme est souvent plus sûr.

Les goroutines sont la première pièce du puzzle de la concurrence en Go. Leur facilité de création et leur légèreté vous permettent d'envisager la concurrence pour un large éventail de problèmes. L'étape suivante consiste à apprendre comment ces goroutines peuvent communiquer et se synchroniser efficacement à l'aide des canaux.