Channels 🚀
O que são Channels? 🤔
Por que usá-los?
- Removem a necessidade de estruturas de sincronização complexas para a comunicação.
- Ajudam a evitar condições de corrida (race conditions) quando bem empregados.
Declaração e Criação de Channels
Você pode declarar um channel com a sintaxe:
var ch chan int
Isso define um channel de inteiros, mas ainda não o inicializa.
Em Go, quando você define um canal (channel) de inteiros, você está apenas declarando a existência desse canal, mas ainda não está criando uma instância dele. Para realmente inicializar o canal e torná-lo utilizável, você precisa usar a função make.
Para criar (inicializar) um channel, utilize a função make:
ch := make(chan int)
Isso cria um channel não-bufferizado que pode ser usado para enviar e receber valores do tipo int.
Operações Básicas com Channels
Envio de dados
Utilize a sintaxe:
ch <- valor
Isso envia “valor” para o channel, bloqueando a execução da goroutine se não houver uma goroutine receptora (no caso de channel não-bufferizado).
Recebimento de dados
Utilize a sintaxe:
valor := <-ch
Isso aguarda e recebe um valor do channel. Se não houver um valor disponível, a goroutine fica bloqueada até que um valor seja enviado.
Exemplo Simples
package main
import (
"fmt"
"time"
)
func main() {
// Cria e inicializa o channel.
ch := make(chan int)
// Lança uma goroutine para enviar um valor.
go func() {
ch <- 42
}()
// Recebe e imprime o valor enviado.
valor := <-ch
fmt.Println("Recebido:", valor)
}
Perceba que não foi necessário um sync.WaitGroup nem um time.Sleep aqui, pois o uso de channels em Go permite a sincronização direta entre goroutines.
No exemplo, a goroutine anônima envia o valor 42 para o channel ch, e a função main aguarda a recepção desse valor antes de continuar a execução.
Isso ocorre porque a operação de envio (ch <- 42) bloqueia a goroutine até que outro goroutine receba o valor, e a operação de recepção (<-ch) bloqueia a função main até que um valor seja enviado.
Dessa forma, o channel garante que a comunicação e sincronização entre as goroutines aconteçam de maneira eficiente e sem a necessidade de mecanismos adicionais de sincronização.
Channels Não-Bufferizados vs. Bufferizados
Em Go existem dois tipos de canais, e um deles a gente já utilizou anteriormente. Vamos nos aprofundar no que tem de diferença entre eles.
Channel não-bufferizado
- Exige que a operação de envio e recebimento ocorram simultaneamente.
- A goroutine que envia fica bloqueada até que outra goroutine esteja pronta para receber.
- Exemplo:
ch := make(chan int)(sem especificar tamanho).
Channel bufferizado
- Permite que um número limitado de valores seja armazenado sem bloquear imediatamente o envio.
- O buffer é definido como o segundo parâmetro da função
make. - Exemplo:
ch := make(chan int, 5)cria um channel que pode armazenar até 5 inteiros antes de bloquear o envio.
Demonstração
Imagine que você tem um channel bufferizado e envia três valores seguidos:
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
Nessa situação, as três operações de envio não bloqueiam, pois há espaço suficiente no buffer. Se você tentar enviar um quarto valor, a operação ficará bloqueada até que algum valor seja recebido.
Fechamento de Channels
Quando fechar um channel
Você deve fechar um channel quando não há mais valores a serem enviados. Isso informa aos receptores que nenhuma outra informação será enviada.
Como fechar
Use a função close:
close(ch) // ch é o nome da variável onde seu channel está armazenado
Recebimento após fechamento
Ao tentar receber de um channel fechado, a operação retornará o valor zero do tipo, além de um flag “ok” que indica se o channel está aberto. Exemplo:
valor, ok := <-ch
if !ok {
fmt.Println("Channel fechado, nenhuma leitura disponível")
}
// Caso `ch` seja um channel de inteiros, a variável `valor` será `0`,
// em uma string será "", etc...
Iterando sobre Channels
Assim como os maps, arrays e slices, também é possível iterar os valores de um channel.
Uso do loop for…range
Permite iterar sobre os valores enviados em um channel até que ele seja fechado. Exemplo:
for v := range ch {
fmt.Println("Valor recebido:", v)
}
Exemplo prático completo
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 5)
// Lança uma goroutine que envia dados e fecha o channel.
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}()
// O loop for...range continua enquanto o channel estiver aberto.
for valor := range ch {
fmt.Println("Recebido:", valor)
}
}
Considerações e Cuidados com Channels
Prevenção de deadlocks
- Certifique-se de que para cada envio haja um receptor, principalmente com channels não-bufferizados.
- Ao usar channels bufferizados, evite encher o buffer sem esvaziá-lo, pois isso pode levar ao bloqueio da goroutine que envia.
Fechamento adequado
- Somente o produtor (quem envia dados) deve fechar o channel.
- Evite fechar o channel de dentro de uma goroutine que também está lendo dele, pois pode causar um
panicse outro produtor tentar enviar.
Boas práticas
- Sempre teste o comportamento do seu código concorrente para identificar e resolver possíveis bloqueios.
- Utilize
valor, ok := <-chpara verificar se o channel foi fechado e tratar a condição apropriadamente.
Exercícios 📝
-
Envio Simples com Channel Não-Bufferizado
- Requisito: Crie um channel de inteiros, lance uma goroutine que envie o valor 100 para o channel e, no main, receba e imprima esse valor.
-
Envio e Recebimento com Channel Bufferizado
- Requisito: Crie um channel bufferizado (capacidade 3), envie três valores seguidos sem causar bloqueio e, em seguida, receba e imprima todos os valores no main.
-
Fechamento de Channel com Iteração
- Requisito: Lance uma goroutine que envie os números de 1 a 5 para um channel, feche o channel e, no main, itere com um loop for…range para imprimir todos os valores recebidos.
-
Verificação de Channel Fechado
- Requisito: Crie um channel, envie um valor, feche-o e, em seguida, no main, use a recepção com
valor, ok := <-chpara imprimir o valor e indicar se o channel ainda está aberto.
- Requisito: Crie um channel, envie um valor, feche-o e, em seguida, no main, use a recepção com
-
Combinação: Goroutine e Channel para Cálculo
- Requisito: Lance uma goroutine que compute a soma dos números de 1 a 5 e envie o resultado por um channel; no main, receba e imprima o resultado.
-
Combinação: WaitGroup e Channel
- Requisito: Lance 2 goroutines que realizem operações simples (por exemplo, uma soma e uma multiplicação) e enviem os resultados por um channel; utilize
sync.WaitGrouppara sincronizar as goroutines e, após sua conclusão, no main receba e imprima os resultados. - Desafio Bônus: Consegue resolver esse exercício sem usar WaitGroups?
- Requisito: Lance 2 goroutines que realizem operações simples (por exemplo, uma soma e uma multiplicação) e enviem os resultados por um channel; utilize
-
Exemplo Completo: Várias Goroutines com WaitGroup e Channel
- Requisito: Crie uma função que lance 5 goroutines, cada uma enviando seu identificador (de 1 a 5) para um channel; use um
WaitGrouppara aguardar que todas terminem e, após isso, itere no main (utilizandofor...range) para imprimir todos os valores do channel. Lembre-se de fechar o channel após a finalização das goroutines. - Desafio Bônus: Consegue resolver esse exercício sem usar WaitGroups?
- Requisito: Crie uma função que lance 5 goroutines, cada uma enviando seu identificador (de 1 a 5) para um channel; use um
Erros Comuns com Channels ⚠️
Operações Bloqueantes em Channels Não-bufferizados
- Erro: Tentar enviar um valor para um channel não-bufferizado sem que haja uma goroutine receptora causando bloqueio (deadlock).
- Solução: Garantir que haja uma goroutine preparada para receber o valor ou usar um channel bufferizado quando adequado.
Esquecer de Fechar um Channel
- Erro: Não fechar um channel quando o produtor termina de enviar valores pode levar a loops infinitos (por exemplo, em um
for…range), pois o receptor ficará aguardando novos valores. - Cuidados: Apenas o produtor deve fechar o channel e sempre fechar após o envio final.
Tentativa de Enviar Dados em um Channel Fechado
- Erro: Enviar valores em um channel que já foi fechado gera um pânico, fazendo com que o programa pare de funcionar.
- Atenção: Feche o channel apenas quando tiver certeza de que nenhum outro envio ocorrerá.
Solução dos exercícios
Pra não ter que ficar rodando manualmente cada exercício, eu usei testes e exemplos testáveis.
Exercício 1:
// main.go
func Ex1() {
ch := make(chan int)
go func() {
ch <- 100
}()
fmt.Println(<-ch)
}
// main_test.go
func ExampleEx1() {
Ex1()
// output: 100
}
Exercício 2:
// main.go
func Ex2() {
ch := make(chan string, 3)
go func() {
ch <- "um"
ch <- "dois"
ch <- "três"
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
// main_test.go
func ExampleEx2() {
Ex2()
// output:
// um
// dois
// três
}
Exercício 3:
// main.go
func Ex3() {
ch := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}
// main_test.go
func ExampleEx3() {
Ex3()
// output:
// 1
// 2
// 3
// 4
// 5
}
Exercício 4:
// main.go
func Ex4() {
ch := make(chan float64)
go func() {
ch <- 10.0
close(ch)
}()
if v, ok := <-ch; ok {
fmt.Printf("%.1f", v)
}
}
// main_test.go
func ExampleEx4() {
Ex4()
// output:
// 10.0
}
Exercício 5:
// main.go
func Ex5() {
ch := make(chan int)
go func() {
n := 5
sum := (n * (n + 1)) / 2
ch <- sum
close(ch)
}()
fmt.Println("Soma:", <-ch)
}
// main_test.go
func ExampleEx5() {
Ex5()
// output:
// Soma: 15
}
Exercício 6:
// main.go
func Ex6() {
ch := make(chan int, 2)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
ch <- 20 + 5
}()
wg.Add(1)
go func() {
defer wg.Done()
ch <- 10 * 10
}()
wg.Wait()
fmt.Println("Primeiro:", <-ch)
fmt.Println("Segundo:", <-ch)
}
// main_test.go
func ExampleEx6() {
Ex6()
// output:
// Primeiro: 100
// Segundo: 25
}
Exercício 7:
// main.go
func Ex7() {
const repeticoes = 5
ch := make(chan int, repeticoes)
wg := &sync.WaitGroup{}
wg.Add(repeticoes)
for i := range repeticoes {
go func(i int) {
defer wg.Done()
ch <- i + 1
}(i)
}
wg.Wait()
close(ch)
for i := range ch {
fmt.Println("Goroutine:", i)
}
}
// main_test.go
func TestEx7(t *testing.T) {
out := captureOutput(Ex7)
expectedOutputs := []string{
"Goroutine: 1",
"Goroutine: 2",
"Goroutine: 3",
"Goroutine: 4",
"Goroutine: 5",
}
for _, expected := range expectedOutputs {
if !strings.Contains(out, expected) {
t.Fatal("Output not generated: ", expected, " got: ", out)
}
}
}
// main_test.go
func captureOutput(f func()) string {
orig := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
os.Stdout = orig
w.Close()
out, _ := io.ReadAll(r)
return string(out)
}
Próximos passos!
Em seguida iremos abordar Select Statements! Fique atento para os próximos dias!