Saiba mais sobre canais em buffer
Como você aprendeu, os canais não são armazenados em buffer por padrão. Isso significa que eles aceitam uma operação de envio somente se houver uma operação de recebimento . Caso contrário, o programa será bloqueado esperando para sempre.
Há momentos em que você precisa desse tipo de sincronização entre goroutines. No entanto, pode haver momentos em que você simplesmente precise implementar a simultaneidade e não precise restringir a forma como as rotinas se comunicam entre si.
Os canais em buffer enviam e recebem dados sem bloquear o programa porque um canal em buffer se comporta como uma fila. Você pode limitar o tamanho dessa fila ao criar o canal, da seguinte forma:
ch := make(chan string, 10)
Toda vez que você envia algo para o canal, o elemento é adicionado à fila. Em seguida, uma operação de recebimento remove o elemento da fila. Quando o canal está cheio, qualquer operação de envio simplesmente espera até que haja espaço para armazenar os dados. Por outro lado, se o canal estiver vazio e houver uma operação de leitura, ele será bloqueado até que haja algo para ler.
Aqui está um exemplo simples para entender os canais em buffer:
package main
import (
"fmt"
)
func send(ch chan string, message string) {
ch <- message
}
func main() {
size := 4
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
send(ch, "three")
send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < size; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
Quando você executa o programa, você vê a seguinte saída:
All data sent to the channel ...
one
two
three
four
Done!
Você pode dizer que não fizemos nada diferente aqui, e você estaria certo. Mas vamos ver quando acontece quando você altera a size
variável para um número menor (você pode até tentar com um número maior), assim:
size := 2
Quando você executa novamente o programa, você recebe o seguinte erro:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.send(...)
/Users/developer/go/src/concurrency/main.go:8
main.main()
/Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2
O motivo é que as chamadas para a send
função são sequenciais. Você não está criando uma nova rotina. Portanto, não há nada para enfileirar.
Os canais estão profundamente ligados às rotinas de goroutines. Sem outra rotina recebendo dados do canal, todo o programa pode entrar em um bloqueio para sempre. Como você viu, isso acontece.
Agora vamos fazer algo interessante! Criaremos uma goroutine para as duas últimas chamadas (as duas primeiras chamadas se encaixam no buffer corretamente) e faremos um loop for executado quatro vezes. Aqui está o código:
func main() {
size := 2
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
go send(ch, "three")
go send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < 4; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
Quando você executa o programa, ele funciona conforme o esperado. Recomendamos que, ao usar canais, você sempre use goroutines.
Vamos testar o caso em que você cria um canal em buffer com mais elementos do que você precisa. Usaremos o exemplo que usamos antes para verificar APIs e criar um canal em buffer com um tamanho de 10:
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, 10)
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 o programa, você obtém a mesma saída que antes. Você pode brincar alterando o tamanho do canal com números menores ou maiores, e o programa ainda funcionará.
Canais sem buffer vs. buffered
Neste ponto, você pode estar se perguntando quando usar um tipo ou outro. Tudo depende de como você quer que a comunicação flua entre as rotinas. Os canais sem buffer comunicam de forma síncrona. Eles garantem que toda vez que você envia dados, o programa é bloqueado até que alguém esteja lendo do canal.
Por outro lado, os canais em buffer separam as operações de envio e recebimento. Eles não bloqueiam um programa, mas você tem que ter cuidado porque você pode acabar causando um impasse (como você viu anteriormente). Quando você usa canais sem buffer, você pode controlar quantas goroutines podem ser executadas simultaneamente. Por exemplo, você pode estar fazendo chamadas para uma API e deseja controlar quantas chamadas realiza a cada segundo. Caso contrário, você pode ser bloqueado.
Direções do canal
Os canais em Go têm outra característica interessante. Ao usar canais como parâmetros para uma função, você pode especificar se um canal deve enviar ou receber dados. À medida que seu programa cresce, você pode ter muitas funções, e é uma boa ideia documentar a intenção de cada canal para usá-las corretamente. Ou talvez você esteja escrevendo uma biblioteca e queira expor um canal como somente leitura para manter a consistência dos dados.
Para definir a direção do canal, faça-o de forma semelhante a quando está a ler ou a receber dados. Mas você faz isso quando você está declarando o canal em um parâmetro de função. A sintaxe para definir o tipo de canal como um parâmetro em uma função é:
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data
Quando você envia dados através de um canal destinado a ser somente para receber, você recebe um erro ao compilar o programa.
Vamos usar o seguinte programa como exemplo de duas funções, uma que lê dados e outra que envia dados:
package main
import "fmt"
func send(ch chan<- string, message string) {
fmt.Printf("Sending: %#v\n", message)
ch <- message
}
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
}
func main() {
ch := make(chan string, 1)
send(ch, "Hello World!")
read(ch)
}
Quando você executa o programa, você vê a seguinte saída:
Sending: "Hello World!"
Receiving: "Hello World!"
O programa esclarece a intenção de cada canal em cada função. Se você tentar usar um canal para enviar dados em um canal cuja finalidade é apenas receber dados, você receberá um erro de compilação. Por exemplo, tente fazer algo assim:
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
ch <- "Bye!"
}
Quando você executa o programa, você vê o seguinte erro:
# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)
É melhor ter um erro de compilação do que usar indevidamente um canal.
Multiplexagem
Finalmente, vamos ver como interagir com mais de um canal simultaneamente usando a select
palavra-chave. Às vezes, você vai querer esperar que um evento aconteça quando estiver trabalhando com vários canais. Por exemplo, você pode incluir alguma lógica para cancelar uma operação quando há uma anomalia nos dados que seu programa está processando.
Uma select
declaração funciona como uma switch
declaração, mas para canais. Ele bloqueia a execução do programa até que ele receba um evento para processar. Se conseguir mais do que um evento, escolhe um aleatoriamente.
Um aspeto essencial da select
declaração é que ela termina sua execução depois de processar um evento. Se você quiser esperar que mais eventos aconteçam, talvez seja necessário usar um loop.
Vamos usar o seguinte programa para ver select
em ação:
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "Done processing!"
}
func replicate(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "Done replicating!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go process(ch1)
go replicate(ch2)
for i := 0; i < 2; i++ {
select {
case process := <-ch1:
fmt.Println(process)
case replicate := <-ch2:
fmt.Println(replicate)
}
}
}
Quando você executa o programa, você vê a seguinte saída:
Done replicating!
Done processing!
Observe que a replicate
função terminou primeiro, e é por isso que você vê sua saída no terminal primeiro. A função principal tem um loop porque a select
instrução termina assim que recebe um evento, mas ainda estamos esperando que a process
função termine.