Utiliser des canaux comme mécanisme de communication

Effectué

Un canal dans Go est un mécanisme de communication entre goroutines. Rappelez-vous que cette approche de l’accès concurrentiel de Go est : « Ne communiquez pas en partageant la mémoire ; au lieu de cela, partagez la mémoire en communiquant. ». Quand vous devez envoyer une valeur d’une goroutine à une autre, vous utilisez des canaux. Voyons comment ils fonctionnent et comment vous pouvez commencer à les utiliser pour écrire des programmes d’accès simultané.

Syntaxe du canal

Comme un canal est un mécanisme de communication qui envoie et reçoit des données, il a également un type qui lui est propre. Cela signifie que vous pouvez envoyer des données uniquement pour le type pris en charge par le canal. Vous utilisez le mot clé chan comme type de données pour un canal, mais vous devez également spécifier le type de données qui passera par le canal, comme un type de int.

Chaque fois que vous déclarez un canal ou que vous souhaitez spécifier un canal en tant que paramètre dans une fonction, vous devez utiliser chan <type>, comme chan int. Pour créer un canal, vous utilisez la fonction intégrée make() :

ch := make(chan int)

Un canal peut effectuer deux opérations : envoyer des données et recevoir des données. Pour spécifier le type d’opération d’un canal, vous devez utiliser l’opérateur de canal <-. En outre, l’envoi et la réception de données dans des canaux bloquent les opérations. Vous verrez dans un moment pourquoi.

Quand vous voulez indiquer qu’un canal envoie seulement des données, utilisez l’opérateur <- après le canal. Quan vous voulez que le canal reçoive des données, utilisez l’opérateur <- avant le canal, comme dans ces exemples :

ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded

Une autre opération que vous pouvez utiliser dans un canal est de le fermer. Pour fermer un canal, utilisez la fonction intégrée close() :

close(ch)

Quand vous fermez un canal, vous indiquez que les données ne seront pas envoyées à nouveau dans ce canal. Si vous tentez d’envoyer des données vers un canal fermé, le programme paniquera. Et si vous tentez de recevoir des données à partir d’un canal fermé, vous pourrez lire toutes les données envoyées. Chaque « lecture » suivante renverra alors une valeur nulle.

Revenons au programme que nous avons créé précédemment et utilisons des canaux pour supprimer la fonctionnalité de veille. Tout d’abord, nous allons créer un canal de chaîne dans la fonction main, comme suit :

ch := make(chan string)

Et nous allons supprimer time.Sleep(3 * time.Second) de la ligne de veille.

Nous pouvons maintenant utiliser les canaux pour communiquer entre les goroutines. Au lieu d’afficher le résultat dans la fonction checkAPI, nous allons refactoriser notre code et envoyer ce message sur le canal. Pour utiliser le canal de cette fonction, vous devez ajouter le canal en tant que paramètre. La fonction checkAPI devrait ressembler à ceci :

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Notez que nous devons utiliser la fonction fmt.Sprintf, car nous ne voulons pas afficher de texte, mais simplement envoyer du texte mis en forme sur le canal. Notez également que nous utilisons l’opérateur <- après la variable de canal pour envoyer des données.

À présent, vous devez modifier la fonction main pour envoyer la variable de canal et recevoir les données pour les imprimer, comme suit :

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)

Notez que nous utilisons l’opérateur <- avant que le canal n’indique que nous souhaitons lire les données à partir du canal.

Lorsque vous réexécutez le programme, vous verrez une sortie semblable à celle-ci :

ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!

Au moins il fonctionne sans appel à une fonction de veille, n’est-ce pas ? Mais cela ne correspond pas à ce que nous recherchons. Nous voyons la sortie uniquement de l’une des goroutines, alors que nous en avons créé cinq. Voyons pourquoi ce programme fonctionne de cette façon dans la section suivante.

Canaux non mis en mémoire tampon

Lorsque vous créez un canal à l’aide de la fonction make(), vous créez un canal non mis en mémoire tampon, qui est le comportement par défaut. Les canaux non mis en mémoire tampon bloquent l’opération d’envoi jusqu’à ce qu’une personne soit prête à recevoir les données. Comme mentionné précédemment, l’envoi et la réception sont des opérations bloquantes. Cette opération bloquante est également la raison pour laquelle le programme de la section précédente s’est arrêté dès qu’il a reçu le premier message.

Nous pouvons commencer par dire que fmt.Print(<-ch) bloque le programme, car la lecture se fait à partir d’un canal et attend que des données arrivent. Dès qu’il y a quelque chose, la lecture se poursuit à la ligne suivante et le programme termine la tâche.

Qu’est-il arrivé au reste des goroutines ? Elles sont toujours en cours d’exécution, mais aucune n’est à l’écoute. Et parce que le programme a terminé sa tâche rapidement, certaines goroutines n’ont pas pu envoyer des données. Pour prouver ce point, nous allons ajouter une autre fmt.Print(<-ch), comme suit :

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)

Lorsque vous réexécutez le programme, vous verrez une sortie semblable à celle-ci :

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!

Notez que vous voyez à présent la sortie de deux API. Si vous continuez à ajouter des lignes fmt.Print(<-ch), vous obtiendrez la lecture de toutes les données envoyées au canal. Mais que se passe-t-il si vous tentez de lire davantage de données et que personne n’envoie plus de données ? Voici un exemple :

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)

fmt.Print(<-ch)

Lorsque vous réexécutez le programme, vous verrez une sortie semblable à celle-ci :

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!

Cela fonctionne, mais le programme ne termine pas la tâche. La dernière ligne est bloquée, car elle attend de recevoir des données. Vous devez fermer le programme avec une commande comme Ctrl+C.

L’exemple précédent prouve simplement que la lecture et la réception de données sont des opérations bloquantes. Pour résoudre ce problème, vous pouvez modifier le code en y définissant une boucle for et recevoir seulement les données que vous êtes sûr d’envoyer, comme dans cet exemple :

for i := 0; i < len(apis); i++ {
    fmt.Print(<-ch)
}

Voici la version finale du programme en cas de problème avec votre version :

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Lorsque vous réexécutez le programme, vous verrez une sortie semblable à celle-ci :

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!

Le programme fait ce qu’il est supposé faire. Vous n’utilisez plus une fonction de veille, vous utilisez des canaux. Notez également qu’il faut environ 600 ms pour effectuer la tâche, au lieu d’environ 2 secondes quand l’accès concurrentiel n’est pas utilisé.

Enfin, nous pourrions indiquer que les canaux non mis en mémoire tampon synchronisent les opérations d’envoi et de réception. Même si vous utilisez la simultanéité, la communication est synchrone.