Golang: Desmistificando channels #1 - O conceito e sintaxe
Introdução
Resolvi fazer esse texto porque no meu primeiro contato com channels eu não entendi muito bem como funcionava, daí eu sempre fugia para o WaitGroup
ou fazia alguma Mutex
para usar goroutines, mesmo quando a melhor solução seria usar channels.
Fiz esse texto com o objetivo de deixar mais claro como usar channels e os problemas comuns que você vai encontrar ao usá-los de forma errada.
O que é e para que serve?
Channel, ou canal, é uma forma nativa do Go para comunicar e sincronizar goroutines.
O channel já é thread-safe por padrão, ou seja, enviar e receber são operações seguras de se usar com goroutines, então você não vai precisar se preocupar em criar mutexes nem nada do tipo.
Com channels você consegue facilmente fazer coisas como:
- Coletar o resultado de várias goroutines fazendo requisições HTTP e ir mostrando na tela
- Limitar seu servidor para processar no máximo 50 requisições concorrentes
- Fazer outras goroutines esperarem por um sinal que sua aplicação iniciou
Criando e usando channels
Para criar um channel, fazemos:
func main() {
ch := make(chan int)
}
O channel pode ser basicamente de qualquer tipo: int
, string
, struct{}
e outros.
Esse tipo é o tipo de dado que vai ser enviado e recebido pelo canal.
Detalhe que ao enviar uma informação para um channel, o que o outro lado recebe é uma cópia do dado.
Dessa forma você já evita vários problemas de data race, porque a goroutine que recebeu o dado pode ler e modificar sem o risco de afetar outra goroutine.
A exceção para isso é quando você cria channels de tipos que não copiam o dado inteiro, como ponteiros, maps e slices.
Exemplos de channels que vão copiar somente a referência, e não o dado completo:
ch := make(chan *int)
ch := make(chan []string)
ch := make(chan map[string]User)
Para enviar informações para um channel, fazemos:
func main() {
ch := make(chan string)
ch <- "Shakespeare"
}
Para receber do channel, fazemos:
func main() {
ch := make(chan string)
// recebendo de um channel e descartando valor
<-ch
// recebendo de um channel e guardando numa variável
name := <-ch
}
Por que não funciona?
Se você rodar qualquer um dos códigos do exemplo anterior, vai acontecer isso:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
main.go:5 +0x31
exit status 2
Isso acontece porque os channels por padrão vão pausar a execução da goroutine que tentar enviar ou receber. Ou seja, a goroutine que tentar enviar vai esperar até que outra esteja pronta para receber, e da mesma forma a goroutine que tentar receber vai esperar até que outra envie.
No caso acima quando eu tento enviar para um channel, eu pauso a goroutine main
para esperar que outra goroutine leia o que enviei, mas como não tem outra goroutine rodando, a main
vai ficar esperando “para sempre” (deadlock
).
O runtime do Go percebe que esse programa vai ficar parado indeterminadamente, porque todas as goroutines estão “dormindo” (all goroutines are asleep
), então ele encerra o programa com um código de erro.
O mesmo acontece no segundo exemplo.
Também existem os buffered channels, que permitem a gente enviar para um canal uma quantidade X de dados antes que ele comece a bloquear a goroutine, mas vamos ver isso mais para frente.
Usando channels corretamente
Para corrigir o problema do deadlock anterior, vamos criar outra goroutine que lê do channel, enquanto a goroutine main
vai enviar para o channel.
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
// obs. 1: 'go' vai fazer com que essa função seja executada numa
// nova goroutine, então ler do channel não vai bloquear a main
go func() {
// obs. 2: eu não preciso passar o ch como parâmetro da função
// porque o ch está no da main escopo
<-ch
}()
// a goroutine main vai esperar até que consiga enviar
ch <- 12
fmt.Println("Fim")
}
Se você está confuso com ch <- 12
e <-ch
, você pode pensar que a seta aponta para onde o dado está fluindo.
// o dado está saindo do channel ch (receber) e sendo descartado
<-ch
// o dado está saindo do channel ch (receber) e sendo atribuído à variável x
x := <-ch
// o dado 12 está indo para o channel ch (enviar)
ch <- 12
Conclusão
Isso conclui a parte 1 dessa série, com o conceito básico de channels.
Na parte 2 vamos ver um exemplo mais realístico de onde e como seria utilizado channels.