Skip to main content


Channels​


Os channels (chan) são recursos do Go que permitem a comunicação entre goroutines, fazendo com que elas possam enviar e receber dados durante a execução do programa.


Quando temos várias goroutines executando de forma concorrente, pode ser necessário compartilhar informações entre elas, para fazer isso, podemos criar um channel, que funciona como um canal de comunicação por onde os dados são transferidos de uma goroutine para outra.


Além de permitir essa comunicação, os channels também ajudam a sincronizar a execução das goroutines, controlando quando determinadas operações podem continuar.


Por exemplo, imagine que uma goroutine seja responsável por consultar informações de um usuário, enquanto outra precisa receber essas informações para continuar seu processamento. Podemos representar essa comunicação assim:

       Goroutine A                    Goroutine B
| |
| |
Consulta usuário |
| |
| Channel |
| |
| --------- Dados -----------> |
| |
| Processa dados
| |

Diferentemente de um slice, que guarda uma sequência de valores, ou de um map, que associa valores a chaves, um channel é usado principalmente para transferir dados e sincronizar a execução entre diferentes partes do programa.



Criando um channel com make(chan)​


Para criar um channel, usamos a função make, informando o tipo de dado que poderá ser enviado e recebido. Por exemplo:

ch := make(chan int)

Nesse exemplo, criamos um channel chamado ch, que permite enviar e receber valores do tipo int. Também podemos criar channels de outros tipos:

// Channel para números inteiros.
numeros := make(chan int)

// Channel para textos.
mensagens := make(chan string)

// Channel para valores booleanos.
resultados := make(chan bool)

O tipo informado na criação define quais valores poderão passar pelo channel. Isso significa que um chan int não pode receber uma string, porque o Go verifica os tipos durante a compilação.


Quando criamos um channel usando make(chan int), ele não possui um buffer para armazenar valores enquanto aguarda o recebimento. Isso significa que o envio de um valor precisa encontrar uma operação de recebimento correspondente para continuar, e o mesmo acontece com quem está recebendo.


Mais adiante veremos a diferença entre channels com e sem buffer.



Enviando e recebendo valores​


Para enviar e receber informações por um channel, usamos o operador <-, que indica a direção em que o valor está sendo transferido. Para enviar um valor, escrevemos:

ch <- 10

Nesse caso, estamos enviando o número 10 para o channel ch. Já para receber um valor, colocamos o operador antes do channel:

numero := <-ch

Agora estamos recebendo um valor do channel e armazenando na variável numero.


Podemos representar assim:

Enviando:

10 ------> ch


Recebendo:

ch ------> numero

O mesmo operador é usado para enviar e receber informações, mas sua posição determina a direção da operação.



Exemplo de comunicação entre goroutines​


Vamos criar um programa onde uma goroutine envia um número por um channel, enquanto a função main() recebe esse valor.

package main

import "fmt"

func main() {
// Cria um channel para transportar números inteiros.
ch := make(chan int)

// Inicia uma goroutine responsável por enviar um valor.
go func() {
ch <- 10
}()

// Recebe o valor enviado pela goroutine.
numero := <-ch

fmt.Println("Número recebido:", numero)
}

Resultado:

Número recebido: 10

Nesse exemplo, primeiro criamos um channel:

ch := make(chan int)

Depois, iniciamos uma goroutine que envia o número 10 para esse channel:

go func() {
ch <- 10
}()

Enquanto isso, a função main() tenta receber um valor:

numero := <-ch

Como nosso channel não possui buffer, a operação de recebimento precisa aguardar até que exista um envio correspondente. Quando a comunicação acontece, o valor é recebido e armazenado na variável numero, permitindo que o programa continue sua execução.



Como funciona o bloqueio de um channel?​


Uma característica dos channels é que suas operações podem bloquear a execução de uma goroutine até que seja possível continuar. Quando trabalhamos com um channel sem buffer, uma operação de envio fica bloqueada até que outra goroutine esteja pronta para receber o valor.


O mesmo acontece com o recebimento. Se tentarmos receber um valor antes que exista um envio correspondente, a goroutine ficará aguardando.

package main

import "fmt"

func main() {
ch := make(chan int)

// Tenta enviar um valor.
ch <- 10

// Tenta receber o valor.
numero := <-ch

fmt.Println(numero)
}

Embora pareça correto enviar um valor e depois recebê-lo, esse programa não funciona. Isso acontece porque ch <- 10 bloqueia a execução até que exista alguém recebendo o valor. Como estamos executando tudo na mesma goroutine, o programa nunca chegará à linha responsável pelo recebimento.


Nesse caso, o Go detecta que todas as goroutines estão bloqueadas e apresenta um erro conhecido como deadlock.

fatal error: all goroutines are asleep - deadlock!

Um deadlock acontece quando as tarefas ficam bloqueadas aguardando condições que não poderão ser atendidas, impedindo que o programa continue. Para resolver esse exemplo, podemos fazer o envio em outra goroutine:

package main

import "fmt"

func main() {
ch := make(chan int)

// Envia o valor em outra goroutine.
go func() {
ch <- 10
}()

// Aguarda o recebimento do valor.
numero := <-ch

fmt.Println(numero)
}

Agora temos duas goroutines que podem participar da comunicação, enquanto uma envia o valor, a outra aguarda para receber ele. Esse comportamento é uma das principais características dos channels, porque permite sincronizar goroutines sem precisar controlar manualmente a ordem de execução.



Diferença entre channels e WaitGroup​


Como já vimos anteriormente, o sync.WaitGroup também permite sincronizar goroutines, mas possui uma responsabilidade diferente. O WaitGroup é usado quando precisamos aguardar a conclusão de uma ou várias goroutines, sem receber diretamente os resultados dessas tarefas.


Já os channels permitem enviar e receber informações entre goroutines, além de sincronizar suas operações de comunicação. Por exemplo, quando precisamos apenas garantir que três tarefas terminaram, podemos usar sync.WaitGroup.


Quando precisamos que uma goroutine processe informações e envie o resultado para outra, podemos usar um channel. Isso não significa que um substitui o outro em todas as situações. Os dois possuem responsabilidades diferentes e podem trabalhar juntos no mesmo programa.



Channels com buffer​


Os channels com buffer permitem armazenar uma quantidade limitada de valores temporariamente, sem precisar que outra goroutine esteja recebendo esses valores no mesmo momento. Diferente dos channels sem buffer (onde o envio precisa esperar por um recebimento), os channels com buffer permitem enviar valores enquanto houver espaço disponível.


Para criar um channel com buffer, usamos make(chan), informando também a quantidade de valores que ele poderá armazenar.

// Channel sem buffer.
ch1 := make(chan int)

// Channel com buffer para 3 valores.
ch2 := make(chan int, 3)

O segundo argumento de make representa a capacidade do buffer, nesse caso, o channel poderá armazenar até três valores antes de bloquear um novo envio. Exemplo:

package main

import "fmt"

func main() {
// Cria um channel com capacidade para 3 valores.
ch := make(chan int, 3)

// Envia os valores para o buffer.
ch <- 10
ch <- 20
ch <- 30

// Recebe os valores na ordem em que foram enviados.
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}

Nesse exemplo, conseguimos enviar três valores sem criar outra goroutine, porque o buffer possui espaço suficiente para armazenar eles. Os valores são recebidos na mesma ordem em que foram enviados, seguindo o comportamento de uma fila, conhecido como FIFO (First In, First Out).



O que acontece quando o buffer fica cheio?​


Quando o buffer atinge sua capacidade máxima, uma nova operação de envio fica bloqueada até que outro valor seja recebido, liberando espaço.

ch := make(chan int, 2)

ch <- 10
ch <- 20

ch <- 30 // Bloqueia, porque o buffer está cheio.

Nesse caso, como não existe outra goroutine recebendo os valores, o programa ficará bloqueado e o Go detectará um deadlock. Para evitar isso, precisamos receber algum valor antes de enviar outro.

ch := make(chan int, 2)

ch <- 10
ch <- 20

<-ch // Recebe o primeiro valor e libera espaço.
ch <- 30 // Agora funciona.


Verificando a capacidade do buffer​


Podemos usar len() para verificar quantos valores estão armazenados no buffer e cap() para consultar sua capacidade total.

ch := make(chan int, 3)

ch <- 10
ch <- 20

fmt.Println(len(ch)) // 2 valores armazenados.
fmt.Println(cap(ch)) // Capacidade para 3 valores.

A capacidade é definida na criação do channel e não aumenta automaticamente. Já a quantidade de valores armazenados muda conforme os envios e recebimentos. Em programas concorrentes, essa quantidade pode mudar a qualquer momento, então len() não deve ser usado para garantir que um envio não ficará bloqueado.


Os channels com buffer são úteis quando precisamos que uma goroutine envie dados sem depender imediatamente de outra para receber eles, porém, quando o buffer ficar cheio, o envio volta a bloquear até que algum espaço seja liberado.



Range e close em channels​


Em Go, podemos usar close() para fechar um channel, indicando que não serão enviados novos valores. Já o range permite percorrer os valores recebidos de um channel, sem precisar definir antecipadamente quantos valores serão recebidos. Esses recursos são úteis quando uma goroutine envia uma quantidade variável de informações e outra precisa processar todos os valores até que o channel seja fechado.



Fechando um channel com close()​


A função close() permite fechar um channel, indicando que não haverá novos envios, porém, fechar um channel não apaga os valores que já estão armazenados no buffer, eles ainda poderão ser recebidos normalmente.

package main

import "fmt"

func main() {
// Cria um channel com buffer para 3 valores.
ch := make(chan int, 3)

ch <- 10
ch <- 20
ch <- 30

// Fecha o channel.
close(ch)

// Recebe os valores que ainda estão no buffer.
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}

Resultado:

10
20
30

Mesmo depois de executar close(ch), conseguimos receber os três valores porque eles já estavam armazenados no buffer, porém, se tentarmos enviar um novo valor depois do fechamento, o Go provocará um panic.

close(ch)

// Provoca um panic porque o channel está fechado.
ch <- 40

Também não podemos fechar o mesmo channel duas vezes, porque isso provoca um panic. O fechamento normalmente deve ser feito pela goroutine responsável pelos envios, quando ela sabe que não haverá mais valores para enviar.



Percorrendo channels com range​


O range permite receber os valores de um channel por meio de um for, sem precisar informar quantas vezes o laço deverá executar. Diferentemente de um slice, onde o range percorre os elementos existentes, em um channel ele aguarda novos valores e continua recebendo até que o channel seja fechado e todos os valores pendentes sejam recebidos.

package main

import "fmt"

func main() {
// Cria um channel para receber números.
ch := make(chan int)

// Inicia uma goroutine responsável pelos envios.
go func() {
ch <- 10
ch <- 20
ch <- 30

// Fecha o channel após terminar os envios.
close(ch)
}()

// Recebe os valores até o channel ser fechado.
for numero := range ch {
fmt.Println(numero)
}

fmt.Println("Channel finalizado!")
}

Resultado:

10
20
30
Channel finalizado!

A goroutine envia três números e depois fecha o channel usando close(ch). Enquanto isso, a função main() percorre os valores recebidos:

for numero := range ch {
fmt.Println(numero)
}

O range aguarda os valores conforme eles são enviados. Quando o channel é fechado e não existem mais valores pendentes, o laço termina automaticamente. Isso significa que não precisamos saber quantos valores serão enviados, apenas garantir que o channel seja fechado quando não houver mais envios.



O que acontece se não fecharmos o channel?​


Se usarmos range em um channel que nunca será fechado, o laço continuará aguardando novos valores, mesmo que todos os envios tenham terminado. No exemplo anterior, se removermos:

close(ch)

A função main() receberá os três números, mas continuará bloqueada esperando outro valor. Como nenhuma goroutine fará novos envios, o Go detectará um deadlock, porém, não precisamos fechar todos os channels obrigatoriamente. O close() é necessário quando queremos sinalizar que não haverá novos valores, principalmente quando usamos range para receber dados até o encerramento do channel.


Também precisamos lembrar que close() não encerra uma goroutine automaticamente. Ele apenas fecha o channel, permitindo que as goroutines responsáveis pelo recebimento identifiquem que não haverá novos envios.



Expressão comma-ok​


A expressão comma-ok é um recurso que permite receber um valor e verificar se uma operação foi realizada corretamente. Já vimos esse comportamento quando trabalhamos com maps, mas também podemos usar essa expressão para verificar se um channel foi fechado. Quando recebemos um valor de um channel, normalmente fazemos:

valor := <-ch

Porém, dessa forma não conseguimos identificar se o valor foi enviado normalmente ou se o channel já estava fechado e vazio. Para resolver isso, podemos receber dois valores:

valor, ok := <-ch

O valor representa o dado recebido do channel, enquanto ok é um booleano que indica se o recebimento foi bem-sucedido. Quando ok é true, recebemos um valor enviado pelo channel, mas quando é false, significa que o channel foi fechado e não possui mais valores pendentes.



Verificando se um channel foi fechado​


Vamos criar um channel com buffer, enviar alguns valores e depois fechá-lo.

package main

import "fmt"

func main() {
// Cria um channel com buffer para 2 valores.
ch := make(chan int, 2)

ch <- 10
ch <- 20

// Fecha o channel após os envios.
close(ch)

// Recebe os valores e verifica o estado do channel.
numero, ok := <-ch
fmt.Println(numero, ok)

numero, ok = <-ch
fmt.Println(numero, ok)

// Tenta receber novamente após esvaziar o channel.
numero, ok = <-ch
fmt.Println(numero, ok)
}

Resultado:

10 true
20 true
0 false

Mesmo depois de executar close(ch), conseguimos receber os valores 10 e 20, porque eles ainda estavam armazenados no buffer. Quando tentamos receber novamente, o channel já está fechado e vazio, então o Go retorna 0, que representa o valor zero do tipo int, e false, indicando que não existem mais valores para receber. Isso acontece porque receber de um channel fechado e vazio não provoca um erro ou panic, mas retorna imediatamente o valor zero do tipo e false.



Por que verificar o ok?​


Imagine que uma goroutine envie o número 0 para um channel.

ch := make(chan int, 1)

ch <- 0
close(ch)

Se recebermos apenas o valor 0, não vamos conseguir saber se ele foi enviado normalmente ou se o channel estava fechado e vazio. Por isso, podemos verificar o segundo retorno:

numero, ok := <-ch

if ok {
fmt.Println("Número recebido:", numero)
} else {
fmt.Println("Channel fechado e vazio")
}

Nesse caso, ok será true, porque o número 0 foi realmente enviado. Se tentarmos receber novamente, ok será false, porque o channel já está fechado e não possui mais valores.


A expressão comma-ok é útil quando precisamos verificar individualmente se ainda existem valores disponíveis em um channel fechado. Já o for range, que estudamos anteriormente, faz essa verificação automaticamente, encerrando o laço quando o channel está fechado e todos os valores pendentes foram recebidos.