Programação Assíncrona em Go: Guia Completo para Iniciantes
Go (Golang) foi criado pensando nas necessidades do desenvolvimento moderno, onde desempenho e concorrência desempenham papéis-chave. Ao contrário de muitas linguagens, onde a assincronia é implementada através de callbacks ou promises, Go oferece mecanismos integrados e elegantes: goroutines e canais. Este guia apresentará os fundamentos da programação assíncrona em Go e mostrará como escrever código concorrente eficiente.
O que são goroutines e como elas diferem das threads?
Goroutine é uma thread de execução leve, gerenciada pelo runtime do Go. Ao contrário das threads do sistema operacional, as goroutines consomem muito menos recursos: sua pilha começa com alguns kilobytes e pode crescer e diminuir dinamicamente. Isso permite executar milhões de goroutines simultaneamente sem sobrecarga significativa de memória.
Iniciar uma goroutine é muito simples — basta adicionar a palavra-chave go antes da chamada de função:
package main
import ( "fmt" "time")
func sayHello() { fmt.Println("Olá da goroutine!")}
func main() { go sayHello() // Iniciando a goroutine time.Sleep(100 * time.Millisecond) // Aguardamos para a goroutine ter tempo de executar fmt.Println("Função principal finalizada")}É importante notar: se a função principal main() for finalizada, todas as goroutines em execução serão interrompidas forçadamente. Para sincronização, usamos ferramentas especiais.
Sincronização com WaitGroup
A maneira mais simples de aguardar a conclusão das goroutines é usar sync.WaitGroup. Ele funciona como um contador: incrementamos ao iniciar uma goroutine e decrementamos ao finalizar. A função principal fica bloqueada até que o contador chegue a zero.
package main
import ( "fmt" "sync" "time")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrementa o contador ao finalizar fmt.Printf("Trabalhador %d começou o trabalho", id) time.Sleep(time.Second) fmt.Printf("Trabalhador %d terminou o trabalho", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Incrementa o contador antes de iniciar go worker(i, &wg) }
wg.Wait() // Aguarda a conclusão de todas as goroutines fmt.Println("Todos os trabalhadores terminaram o trabalho")}Observe: wg.Add(1) é chamado antes de iniciar a goroutine, e wg.Done() é chamado dentro dela (geralmente via defer). Isso garante que o contador seja decrementado corretamente mesmo em caso de pânico.
Canais: comunicação entre goroutines
Canais (channels) são "tubos" tipados através dos quais as goroutines podem trocar dados com segurança. Os canais fornecem sincronização por padrão: o envio para um canal bloqueia a goroutine até que outra goroutine leia o valor, e vice-versa.
Os canais são criados com a função make:
package main
import "fmt"
func main() { // Criando um canal não bufferizado para inteiros ch := make(chan int)
// Iniciando uma goroutine que envia dados go func() { ch <- 42 // Envia o valor para o canal }()
// Lendo do canal (bloqueia até que um valor chegue) value := <-ch fmt.Println("Valor recebido:", value)}Os canais podem ser:
- Não bufferizados — síncronos, o envio bloqueia até o recebimento.
- Bufferizados — assíncronos, o envio bloqueia apenas quando o buffer está cheio.
// Criando um canal bufferizado para 3 elementosbufferedCh := make(chan string, 3)bufferedCh <- "A"bufferedCh <- "B"bufferedCh <- "C" // Enquanto não estiver cheio — não bloqueia