Utilizar os canais como mecanismo de comunicação

Concluído

Um canal em Go é um mecanismo de comunicação entre goroutines. Lembre-se que a abordagem de Go para simultaneidade é: "Não se comunique compartilhando memória, em vez disso, compartilhe memória comunicando". Quando você precisa enviar um valor de uma rotina para outra, você usa canais. Vamos ver como eles funcionam e como você pode começar a usá-los para escrever programas Go simultâneos.

Sintaxe do canal

Como um canal é um mecanismo de comunicação que envia e recebe dados, ele também tem um tipo. Isso significa que você pode enviar dados apenas para o tipo que o canal suporta. Você usa a palavra-chave chan como o tipo de dados para um canal, mas também precisa especificar o tipo de dados que passará pelo canal, como um int tipo.

Toda vez que você declarar um canal ou quiser especificar um canal como um parâmetro em uma função, você precisa usar chan <type>, como chan int. Para criar um canal, use a função interna make() :

ch := make(chan int)

Um canal pode fazer duas operações: enviar dados e receber dados. Para especificar o tipo de operação que um canal tem, você precisa usar o operador <-de canal . Além disso, o envio e o recebimento de dados em canais estão bloqueando as operações. Você verá em um momento o porquê.

Quando você quiser dizer que um canal só envia dados, use o <- operador depois do canal. Quando você quiser que o canal receba dados, use o <- operador antes do canal, como nestes exemplos:

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

Outra operação que você pode usar em um canal é fechá-lo. Para fechar um canal, use a função integrada close() :

close(ch)

Quando você fecha um canal, está dizendo que os dados não serão enviados nesse canal novamente. Se você tentar enviar dados para um canal fechado, o programa entrará em pânico. E se tentar receber dados de um canal fechado, poderá ler todos os dados enviados. Cada "leitura" subsequente retornará um valor zero.

Vamos voltar ao programa que criamos antes e usar canais para remover a funcionalidade de suspensão. Primeiro, vamos criar um canal de cadeia de caracteres na main função, assim:

ch := make(chan string)

E vamos remover a linha time.Sleep(3 * time.Second)de sono.

Agora, podemos usar canais para nos comunicarmos entre goroutines. Em vez de imprimir o resultado na checkAPI função, vamos refatorar nosso código e enviar essa mensagem pelo canal. Para usar o canal dessa função, você precisa adicionar o canal como parâmetro. A checkAPI função deve ter esta aparência:

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)
}

Observe que temos que usar a fmt.Sprintf função porque não queremos imprimir nenhum texto, apenas enviar texto formatado através do canal. Além disso, observe que estamos usando o <- operador após a variável de canal para enviar dados.

Agora você precisa alterar a main função para enviar a variável de canal e receber os dados para imprimi-la, assim:

ch := make(chan string)

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

fmt.Print(<-ch)

Observe como estamos usando o <- operador antes que o canal diga que queremos ler os dados do canal.

Quando você executa novamente o programa, você vê uma saída como esta:

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

Done! It took 0.007401217 seconds!

Pelo menos está funcionando sem uma chamada para uma função de sono, certo? Mas ainda não está a fazer o que queremos. Estamos vendo a saída apenas de um dos goroutines, e criamos cinco. Vamos ver por que este programa está funcionando dessa forma na próxima seção.

Canais sem buffer

Quando você cria um canal usando a make() função, você cria um canal sem buffer, que é o comportamento padrão. Os canais sem buffer bloqueiam a operação de envio até que haja alguém pronto para receber os dados. Como dissemos anteriormente, enviar e receber são operações de bloqueio. Esta operação de bloqueio também é por isso que o programa da seção anterior parou assim que recebeu a primeira mensagem.

Podemos começar dizendo que fmt.Print(<-ch) bloqueia o programa porque está lendo de um canal, e está esperando que alguns dados cheguem. Assim que tiver algo, continua com a próxima linha, e o programa termina.

O que aconteceu com o resto das rotinas? Eles ainda estão correndo, mas ninguém está mais ouvindo. E como o programa terminou cedo, algumas goroutines não conseguiram enviar dados. Para provar este ponto, vamos acrescentar outro fmt.Print(<-ch), como este:

ch := make(chan string)

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

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

Quando você executa novamente o programa, você vê uma saída como esta:

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

Observe que agora você vê a saída para duas APIs. Se você continuar adicionando mais fmt.Print(<-ch) linhas, acabará lendo todos os dados que estão sendo enviados para o canal. Mas o que acontece se você tentar ler mais dados e ninguém mais enviar dados? Um exemplo é mais ou menos assim:

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)

Quando você executa novamente o programa, você vê uma saída como esta:

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!

Está funcionando, mas o programa não termina. A última linha de impressão está a bloqueá-lo porque espera receber dados. Você terá que fechar o programa com um comando como Ctrl+C.

O exemplo anterior apenas prova que ler e receber dados são operações de bloqueio. Para corrigir esse problema, você pode alterar o código para um for loop e receber apenas os dados que você tem certeza de que está enviando, como este exemplo:

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

Aqui está a versão final do programa no caso de algo dar errado com a sua versão:

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)
}

Quando você executa novamente o programa, você vê uma saída como esta:

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!

O programa está fazendo o que deveria fazer. Você não está mais usando uma função de suspensão, você está usando canais. Observe também que agora leva cerca de 600 ms para terminar, em vez de quase 2 segundos, quando não estávamos usando simultaneidade.

Finalmente, podemos dizer que os canais sem buffer estão sincronizando as operações de envio e recebimento. Mesmo que você esteja usando simultaneidade, a comunicação é síncrona.