Leçon 4/6 8 min

Goroutines et channels

Découvrez la concurrence en Go : goroutines, channels, select et sync.WaitGroup.

Concurrence vs parallélisme

Imaginez un cuisinier seul dans une cuisine. Il peut préparer la salade pendant que l'eau bout et que le poulet est au four. Il ne fait qu'une chose à la fois, mais il jongle entre plusieurs tâches. C'est la concurrence.

Maintenant imaginez trois cuisiniers qui préparent chacun un plat en même temps. C'est le parallélisme.

Go excelle dans les deux. Et il rend la concurrence ridiculement simple avec un seul mot-clé : go.

Les goroutines : la concurrence en un mot

Une goroutine est une fonction qui s'exécute en arrière-plan. Pour la lancer, ajoutez go devant l'appel :

func afficher(msg string) {
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go afficher("goroutine")  // S'exécute en arrière-plan
    afficher("main")          // S'exécute au premier plan
}

Les deux fonctions s'exécutent en même temps. La sortie peut varier à chaque exécution :

Sortie possible :

main
goroutine
goroutine
main
main
goroutine

Une goroutine coûte environ 2 Ko de mémoire. Un thread système en coûte 1 Mo. Vous pouvez créer des millions de goroutines sans problème — c'est comme ça que Go gère des milliers de connexions simultanées.

Les channels : communiquer entre goroutines

Les goroutines travaillent en parallèle, mais comment échangent-elles des données ? Via des channels — des tuyaux typés :

func calculer(ch chan int) {
    resultat := 42 * 2
    ch <- resultat  // Envoyer dans le channel
}

func main() {
    ch := make(chan int)       // Créer un channel d'entiers
    go calculer(ch)            // Lancer la goroutine
    resultat := <-ch           // Recevoir du channel (bloquant)
    fmt.Println(resultat)      // 84
}

L'opérateur <- fonctionne dans les deux sens :

  • ch <- valeur — envoyer une valeur dans le channel
  • valeur := <-ch — recevoir une valeur du channel

La réception est bloquante : le programme attend jusqu'à ce qu'une valeur arrive. Pas besoin de mutex ou de verrous compliqués.

Le proverbe de Go : "Ne communiquez pas en partageant de la mémoire. Partagez de la mémoire en communiquant."

select : écouter plusieurs channels

select permet d'attendre sur plusieurs channels en même temps, comme un switch pour les channels :

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "résultat 1"
    }()
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "résultat 2"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("Reçu de ch1:", msg)
    case msg := <-ch2:
        fmt.Println("Reçu de ch2:", msg)
    }
}

select exécute le premier channel qui est prêt. C'est la base des serveurs web performants et des systèmes temps réel.

sync.WaitGroup : attendre toutes les goroutines

Comment savoir quand toutes vos goroutines ont terminé ? Avec sync.WaitGroup :

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)  // +1 goroutine à attendre
        go func(n int) {
            defer wg.Done()  // -1 quand terminé
            fmt.Printf("Tâche %d terminée\n", n)
        }(i)
    }

    wg.Wait()  // Attendre que toutes soient terminées
    fmt.Println("Tout est fini !")
}

Trois méthodes : Add(n) ajoute n tâches, Done() signale qu'une tâche est terminée, Wait() bloque jusqu'à ce que le compteur atteigne zéro.

Concurrency vs parallelism

Imagine a single cook in a kitchen. They can prepare the salad while the water boils and the chicken is in the oven. They only do one thing at a time, but they juggle multiple tasks. That's concurrency.

Now imagine three cooks each preparing a dish at the same time. That's parallelism.

Go excels at both. And it makes concurrency ridiculously simple with a single keyword: go.

Goroutines: concurrency in one word

A goroutine is a function that runs in the background. To launch it, add go before the call:

func display(msg string) {
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go display("goroutine")  // Runs in the background
    display("main")          // Runs in the foreground
}

Both functions execute at the same time. The output may vary each time:

Possible output:

main
goroutine
goroutine
main
main
goroutine

A goroutine costs about 2 KB of memory. A system thread costs 1 MB. You can create millions of goroutines without issues — that's how Go handles thousands of simultaneous connections.

Channels: communicating between goroutines

Goroutines work in parallel, but how do they exchange data? Through channels — typed pipes:

func compute(ch chan int) {
    result := 42 * 2
    ch <- result  // Send to the channel
}

func main() {
    ch := make(chan int)       // Create a channel of integers
    go compute(ch)             // Launch the goroutine
    result := <-ch             // Receive from channel (blocking)
    fmt.Println(result)        // 84
}

The <- operator works both ways:

  • ch <- value — send a value to the channel
  • value := <-ch — receive a value from the channel

Receiving is blocking: the program waits until a value arrives. No need for mutexes or complicated locks.

Go's proverb: "Don't communicate by sharing memory. Share memory by communicating."

select: listening to multiple channels

select lets you wait on multiple channels at once, like a switch for channels:

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "result 1"
    }()
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "result 2"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("Received from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("Received from ch2:", msg)
    }
}

select executes the first channel that's ready. It's the foundation of performant web servers and real-time systems.

sync.WaitGroup: waiting for all goroutines

How do you know when all your goroutines are done? With sync.WaitGroup:

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)  // +1 goroutine to wait for
        go func(n int) {
            defer wg.Done()  // -1 when done
            fmt.Printf("Task %d completed\n", n)
        }(i)
    }

    wg.Wait()  // Wait until all are done
    fmt.Println("All done!")
}

Three methods: Add(n) adds n tasks, Done() signals a task is complete, Wait() blocks until the counter reaches zero.

Testez dans le Go Playground

Testez les goroutines et channels dans le Go Playground :

Avec l'IA

Copiez ce prompt dans Claude ou ChatGPT :

Écris un programme Go qui télécharge 5 URLs en parallèle avec des goroutines et channels. Affiche le temps de réponse de chaque URL. Utilise un WaitGroup.
Comment lancer une goroutine ?
Combien de mémoire coûte une goroutine environ ?
Que fait l'opérateur <- avec un channel ?
Besoin d'un développeur pour votre projet ?

Réponse sous 24h — Sans engagement