Saiba mais sobre canais em buffer

Concluído

Como você aprendeu, os canais são sem buffer por padrão. Isso significa que eles aceitam uma operação de envio somente quando há uma operação de recebimento. Caso contrário, o programa é bloqueado aguardando para sempre.

Há ocasiões em que você precisa desse tipo de sincronização entre goroutines. No entanto, pode haver casos em que você simplesmente precisa implementar a simultaneidade e não precisa restringir como as goroutines se comunicam entre si.

Os canais armazenados 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 maneira:

ch := make(chan string, 10)

Sempre que você envia algo ao canal, o elemento é adicionado à fila. Em seguida, uma operação de recebimento remove o elemento da fila. Quando o canal estiver cheio, qualquer operação de envio simplesmente aguardará até que haja espaço para manter os dados. Por outro lado, se o canal estiver vazio e houver uma operação de leitura, ela será bloqueada até que haja algo a ser lido.

Veja 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!")
}

Ao executar o programa, você verá 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 estaria certo. Porém, vamos ver o que acontece quando você altera a variável size para um número mais baixo (você pode até mesmo tentar com um número mais alto), desta forma:

size := 2

Ao executar o programa novamente, 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 função send são sequenciais. Você não está criando uma goroutine. Portanto, não há nada para enfileirar.

Os canais estão profundamente conectados às goroutines. Sem outra goroutine recebendo dados do canal, todo o programa pode entrar em um bloqueio para sempre. Como você viu, isso acontece.

Agora, vamos fazer algo interessante! Vamos criar uma goroutine para as duas últimas chamadas (as duas primeiras cabem no buffer corretamente) e fazer um loop for ser executado quatro vezes. O código é o seguinte:

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 o necessário. Usaremos o exemplo que usamos antes para verificar as APIs e criar um canal em buffer com tamanho 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)
}

Ao executar o programa, você obterá a mesma saída que antes. Você pode experimentar o tamanho do canal com números mais baixos ou mais altos e o programa ainda funcionará.

Canais sem buffer versus em buffer

Neste ponto, você deve estar se perguntando quando usar um tipo ou outro. Tudo depende de como você deseja que a comunicação flua entre as goroutines. Os canais sem buffer se comunicam de maneira síncrona. Eles garantem que, sempre que você enviar dados, o programa será bloqueado até que alguém esteja lendo do canal.

Por outro lado, os canais em buffer desassociam as operações de envio e recebimento. Eles não bloqueiam um programa, mas você precisa ter cuidado porque pode acabar causando um deadlock (como visto antes). Ao usar 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 executa a cada segundo. Caso contrário, você poderá ser bloqueado.

Direções de canal

Os canais no Go têm outro recurso interessante. Quando você usa canais como parâmetros em uma função, é possível especificar se um canal é destinado a enviar ou a 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.

A direção do canal é definida de maneira semelhante a quando você está lendo ou recebendo dados. Porém, você faz isso quando 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

Ao enviar dados por meio de um canal destinado a somente receber, você recebe um erro ao compilar o programa.

Vamos usar o seguinte programa como um exemplo de duas funções, uma que lê os 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)
}

Ao executar o programa, você verá 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, 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!"
}

Ao executar o programa, você verá 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 fazer uso indevido de um canal.

Multiplexação

Por fim, vamos ver como interagir com mais de um canal ao mesmo tempo usando a palavra-chave select. Às vezes, você quer aguardar a ocorrência de um evento ao trabalhar 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 instrução select funciona como uma instrução switch, mas para canais. Ela bloqueia a execução do programa até receber um evento a ser processado. Se ela obtiver mais de um evento, escolherá um aleatoriamente.

Um aspecto essencial da instrução select é que ela conclui sua execução depois de processar um evento. Se você quiser aguardar mais eventos ocorrerem, 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)
        }
    }
}

Ao executar o programa, você verá a seguinte saída:

Done replicating!
Done processing!

Observe que a função replicate foi concluída primeiro, motivo pelo qual você vê a saída dela no terminal primeiro. A função main tem um loop porque a instrução select termina assim que recebe um evento, mas ainda estamos aguardando a conclusão da função process.