Usar canais como um mecanismo de comunicação
Um canal no Go é um mecanismo de comunicação entre goroutines. Lembre-se de que a abordagem de simultaneidade do Go é: "Não nos comunicamos por compartilhamento de memória; em vez disso, compartilhamos a memória ao nos comunicarmos". Quando precisar enviar um valor de uma goroutine para outra, você usa os canais. Vamos ver como eles funcionam e como você pode começar a usá-los para escrever programas simultâneos em Go.
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 ao qual o canal dá suporte. 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 tipo int
.
Sempre que você declarar um canal ou quiser especificar um canal como um parâmetro em uma função, precisará usar chan <type>
, como chan int
. Para criar um canal, use a função interna make()
:
ch := make(chan int)
Um canal pode realizar duas operações: enviar dados e receber dados. Para especificar o tipo de operação de um canal, use o operador de canal <-
. Além disso, o envio de dados e o recebimento de dados em canais são operações de bloqueio. Você verá por que daqui a pouco.
Quando quer dizer que um canal só envia dados, use o operador <-
após o canal. Quando 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 é para fechá-lo. Para fechar um canal, use a função interna close()
:
close(ch)
Ao fechar um canal, você está dizendo que os dados não serão mais enviados por meio dele. Se você tentar enviar dados para um canal fechado, o programa entrará em pane. Se você tentar receber dados de um canal fechado, poderá ler todos os dados enviados. Cada "leitura" subsequente então retornará um valor zero.
Vamos voltar ao programa que criamos antes e usar os canais para remover a funcionalidade de suspensão. Primeiro, vamos criar um canal de cadeia de caracteres na função main
, da seguinte maneira:
ch := make(chan string)
Vamos remover a linha de suspensão time.Sleep(3 * time.Second)
.
Agora, podemos usar canais para comunicação entre goroutines. Em vez de imprimir o resultado na função checkAPI
, vamos refatorar nosso código e enviar essa mensagem pelo canal. Para usar o canal dessa função, você precisa adicionar o canal como o parâmetro. A função checkAPI
deve ter a seguinte 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 precisamos usar a função fmt.Sprintf
porque não queremos imprimir nenhum texto, basta enviar texto formatado pelo 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 função main
para enviar a variável de canal e receber os dados para imprimi-los, desta forma:
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
Observe como estamos usando o operador <-
antes de o canal dizer que queremos ler dados do canal.
Ao executar o programa novamente, você verá uma saída como esta:
ERROR: https://api.somewhereintheinternet.com/ is down!
Done! It took 0.007401217 seconds!
Pelo menos ele está trabalhando sem uma chamada para uma função de suspensão, certo? Porém, ainda não está fazendo o que queremos. Estamos vendo a saída apenas de uma das goroutines, e nós criamos cinco. Na próxima seção, vamos ver por que esse programa está funcionando assim.
Canais sem buffer
Ao criar um canal usando a função make()
, você cria um canal sem buffer, que é o comportamento padrão. Canais sem buffer bloqueiam a operação de envio até que alguém esteja pronto para receber os dados. Como dissemos antes: o envio e o recebimento são operações de bloqueio. Essa operação de bloqueio também foi o motivo pelo qual 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 ele está lendo de um canal e está aguardando a chegada de alguns dados. Assim que tiver algo, ele prosseguirá para a próxima linha e o programa será concluído.
O que aconteceu com o restante das goroutines? Elas ainda estão em execução, mas ninguém está ouvindo mais. E como o programa foi concluído de modo antecipado, algumas goroutines não conseguiram enviar dados. Para provar isso, vamos adicionar outro fmt.Print(<-ch)
, desta forma:
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
fmt.Print(<-ch)
Ao executar o programa novamente, você verá 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 de duas APIs. Se você continuar adicionando mais linhas fmt.Print(<-ch)
, acabará lendo todos os dados que estão sendo enviados para o canal. No entanto, o que acontecerá se você tentar ler mais dados e ninguém mais estiver enviando dados? Um exemplo é algo 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)
Ao executar o programa novamente, você verá 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!
Ele está funcionando, mas o programa não é concluído. A última linha de impressão o está bloqueando porque está esperando receber dados. Você precisará fechar o programa com um comando como Ctrl+C
.
O exemplo anterior apenas comprova que a leitura e o recebimento de dados são operações de bloqueio. Para corrigir esse problema, você poderia alterar o código para um loop for
e receber apenas os dados que tem certeza de que está enviando, como neste exemplo:
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
Esta é a versão final do programa, caso algo tenha dado errado com 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)
}
Ao executar o programa novamente, você verá 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, mas sim canais. Observe também que agora leva cerca de 600 ms para ser concluído, em vez dos quase 2 segundos de quando não estávamos usando a simultaneidade.
Por fim, poderíamos dizer que os canais sem buffer estão sincronizando as operações de envio e recebimento. Embora você esteja usando a simultaneidade, a comunicação é síncrona.