Saiba mais sobre canais em buffer

Concluído

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.