Contactez-nous

Canaux (Channels) : communication basique (`make`, `<-`, `close`)

Apprenez les bases des canaux (channels) Go : création avec make, envoi/réception de données via <-, et fermeture avec close pour une communication sûre entre goroutines.

Le pont entre Goroutines : Introduction aux Canaux (Channels)

Nous savons maintenant lancer des tâches concurrentes avec les goroutines. Mais comment faire pour que ces tâches indépendantes communiquent entre elles ou coordonnent leurs actions ? Plutôt que de se baser principalement sur le partage de mémoire et l'utilisation de verrous (mutex), qui peuvent être complexes et sources d'erreurs, Go promeut une approche différente encapsulée dans le célèbre adage : "Ne communiquez pas en partageant la mémoire ; partagez la mémoire en communiquant."

Le mécanisme principal pour réaliser cette communication en Go est le canal (channel). Un canal est un conduit typé à travers lequel vous pouvez envoyer et recevoir des valeurs entre goroutines, en utilisant l'opérateur `<-`. Les canaux fournissent non seulement un moyen de transférer des données, mais aussi un mécanisme intégré de synchronisation, car les opérations d'envoi et de réception sont bloquantes par défaut.

Pensez à un canal comme à un tuyau de communication spécifique à un type de données (par exemple, un tuyau pour envoyer des `int`, un autre pour envoyer des `string`). Les goroutines peuvent "brancher" une extrémité pour envoyer des données et une autre pour en recevoir. Ce sont des citoyens de première classe en Go, ce qui signifie que vous pouvez les créer, les passer comme arguments de fonction, les retourner depuis des fonctions, etc.

Créer un Canal : La fonction `make`

Comme pour les slices et les maps, les canaux doivent être créés avant d'être utilisés, à l'aide de la fonction intégrée `make`. La syntaxe spécifie le mot-clé `chan` suivi du type de données que le canal transportera.

Syntaxe (canal non-bufferisé) : `monCanal := make(chan TypeElement)`

package main

import "fmt"

func main() {
    // Crée un canal qui transportera des entiers (int)
    canalInt := make(chan int)
    fmt.Printf("Type de canalInt: %T, valeur: %v\n", canalInt, canalInt)

    // Crée un canal qui transportera des chaînes (string)
    canalString := make(chan string)
    fmt.Printf("Type de canalString: %T, valeur: %v\n", canalString, canalString)

    // Crée un canal qui transportera des booléens (bool)
    canalBool := make(chan bool)
    fmt.Printf("Type de canalBool: %T, valeur: %v\n", canalBool, canalBool)
}
Par défaut, `make` crée un canal non-bufferisé (unbuffered). Cela signifie que le canal n'a pas de capacité de stockage interne. Une opération d'envoi sur un canal non-bufferisé bloquera la goroutine envoyante jusqu'à ce qu'une autre goroutine soit prête à recevoir sur ce même canal. Inversement, une réception bloquera jusqu'à ce qu'un envoi soit effectué. C'est ce comportement qui assure la synchronisation.

Il est aussi possible de créer des canaux bufferisés (`make(chan Type, capacité)`) qui peuvent stocker un nombre limité de valeurs sans bloquer l'expéditeur, mais cela sort du cadre de cette introduction basique. Nous nous concentrerons sur les canaux non-bufferisés.

Envoyer et Recevoir des Données : L'opérateur `<-`

L'opérateur flèche `<-` est utilisé à la fois pour envoyer et recevoir des données sur un canal.

1. Envoyer une valeur : La flèche pointe vers le canal. `nomCanal <- valeur`

// Envoyer la valeur 42 sur canalInt
canalInt <- 42

// Envoyer la chaîne "hello" sur canalString
canalString <- "hello"
Rappel : sur un canal non-bufferisé, cette opération bloque jusqu'à ce qu'une goroutine tente de recevoir (`<- nomCanal`).

2. Recevoir une valeur : La flèche pointe depuis le canal. `variable := <- nomCanal` ou simplement `<- nomCanal` (si la valeur n'est pas utilisée).

// Recevoir une valeur de canalInt et la stocker dans 'val'
val := <- canalInt

// Recevoir une valeur de canalString et l'ignorer
<- canalString
Rappel : sur un canal non-bufferisé, cette opération bloque jusqu'à ce qu'une goroutine tente d'envoyer (`nomCanal <- ...`).

Voyons un exemple complet très simple illustrant l'envoi et la réception entre deux goroutines (la principale et une nouvelle) :

package main

import (
    "fmt"
    "time"
)

func main() {
    messages := make(chan string) // Canal non-bufferisé de strings

    // Lance une goroutine qui enverra un message sur le canal
    go func() {
        fmt.Println("(Goroutine) Préparation de l'envoi...")
        time.Sleep(1 * time.Second) // Simule un travail
        messages <- "Ping!" // Envoi sur le canal (bloque jusqu'à la réception)
        fmt.Println("(Goroutine) Message envoyé.")
    }()

    fmt.Println("(Main) Attente de réception...")
    // Réception depuis le canal (bloque jusqu'à l'envoi par la goroutine)
    msg := <- messages 
    fmt.Printf("(Main) Message reçu: %s\n", msg)

    fmt.Println("(Main) Fin du programme.")
}
Sortie probable :
(Main) Attente de réception...
(Goroutine) Préparation de l'envoi...
(Goroutine) Message envoyé.
(Main) Message reçu: Ping!
(Main) Fin du programme.
Dans cet exemple, `main` bloque sur `msg := <- messages`. La goroutine anonyme bloque sur `messages <- "Ping!"`. L'envoi et la réception se "rencontrent", la valeur est transférée, puis les deux goroutines peuvent continuer.

Signaler la fin des envois : Fermer un Canal (`close`)

Il est souvent nécessaire pour l'expéditeur de signaler aux récepteurs qu'il n'enverra plus de valeurs sur le canal. Cela se fait en fermant le canal à l'aide de la fonction intégrée `close`.

Syntaxe : `close(nomCanal)`

Règles importantes concernant `close` :
  • Seul l'expéditeur doit fermer un canal. Tenter de fermer un canal depuis un récepteur ou fermer un canal déjà fermé provoquera une panique.
  • Après la fermeture d'un canal, aucune autre valeur ne peut y être envoyée. Tenter d'envoyer (`<-`) sur un canal fermé provoque une panique.
  • Les opérations de réception (`<-`) sur un canal fermé sont toujours possibles et ne bloquent jamais. Elles retournent immédiatement :
    • Les valeurs qui étaient éventuellement encore dans le buffer (pour les canaux bufferisés).
    • Une fois le buffer vide (ou pour un canal non-bufferisé), elles retournent la valeur zéro du type du canal, indéfiniment.

Exemple simple de fermeture :

jobs := make(chan int, 5) // Canal bufferisé (pour l'exemple)
done := make(chan bool)

go func() {
    for i := 1; i <= 3; i++ {
        fmt.Printf("(Sender) Envoi du job %d\n", i)
        jobs <- i
        time.Sleep(50 * time.Millisecond)
    }
    fmt.Println("(Sender) Plus de jobs à envoyer. Fermeture du canal.")
    close(jobs) // L'expéditeur ferme le canal
}()

go func() {
    for {
        // Réception avec vérification (voir section suivante)
        j, more := <-jobs 
        if more {
            fmt.Printf("(Receiver) Reçu job %d\n", j)
        } else {
            fmt.Println("(Receiver) Canal fermé. Tous les jobs reçus.")
            done <- true // Signale la fin au main
            return // Sort de la boucle
        }
    }
}()

// Attend que le récepteur signale la fin
<-done 
fmt.Println("Programme terminé.")

Détecter un canal fermé lors de la réception : Le "comma ok"

Comme la réception sur un canal fermé retourne la valeur zéro du type, comment un récepteur peut-il distinguer une valeur zéro légitimement envoyée de la valeur zéro signalant la fermeture ? En utilisant la forme spéciale de réception à deux valeurs, similaire à celle des maps :

Syntaxe : `valeur, ok := <- nomCanal`

  • `valeur` : contient la valeur reçue (ou la valeur zéro si le canal est fermé et vide).
  • `ok` : est un booléen. `true` si la `valeur` a été reçue suite à un envoi réussi (avant la fermeture). `false` si la réception a retourné une valeur zéro parce que le canal est fermé et vide.

C'est le moyen idiomatique pour un récepteur de savoir quand arrêter de lire sur un canal. L'exemple précédent utilisait déjà ce pattern :

j, more := <-jobs // 'more' est notre variable 'ok'
if more { // Si ok == true, c'était une vraie valeur
    fmt.Printf("(Receiver) Reçu job %d\n", j)
} else { // Si ok == false, le canal est fermé
    fmt.Println("(Receiver) Canal fermé. Tous les jobs reçus.")
    done <- true
    return
}
Ce test `if more` permet de sortir proprement de la boucle de réception lorsque le canal `jobs` est fermé par l'expéditeur.

Conclusion : Communication sûre et synchronisée

Les canaux sont le mécanisme fondamental et idiomatique pour la communication et la synchronisation entre goroutines en Go. Ils offrent un moyen typé et sûr d'échanger des données.

Retenez les opérations de base :

  • Création : `make(chan Type)`.
  • Envoi : `canal <- valeur` (bloquant sur canal non-bufferisé).
  • Réception : `valeur := <- canal` (bloquant sur canal non-bufferisé).
  • Fermeture : `close(canal)` (par l'expéditeur, signale la fin).
  • Détection de fermeture : `valeur, ok := <- canal`.

Leur nature bloquante intrinsèque (pour les canaux non-bufferisés) fournit un puissant mécanisme de synchronisation sans nécessiter de verrous explicites dans de nombreux cas. Maîtriser les canaux est une étape clé pour exploiter pleinement les capacités de concurrence de Go.