Programação Assíncrona em Go: Conceitos Básicos e Prática
Go (Golang) foi projetado desde o início como uma linguagem para trabalhar eficientemente com tarefas concorrentes. Ao contrário de muitas outras linguagens, onde a assincronia é alcançada através de callbacks ou bibliotecas complexas, Go oferece ferramentas simples e elegantes: goroutines, channels e o operador select. Neste guia, vamos analisar como funciona a programação assíncrona em Go e mostrar exemplos práticos.
1. Goroutines: Threads de Execução Leves
Uma goroutine é uma thread de execução leve, gerenciada pelo runtime do Go. Ao contrário das threads do sistema, as goroutines têm uma sobrecarga mínima. Você pode executar milhares de goroutines sem impacto significativo no desempenho.
Para iniciar uma goroutine, usa-se a palavra-chave go antes da chamada de função:
package main
import ( "fmt" "time")
func printMessage(msg string) { for i := 0; i < 5; i++ { fmt.Println(msg) time.Sleep(100 * time.Millisecond) }}
func main() { // Iniciando uma goroutine go printMessage("Saída Assíncrona") // A thread principal continua o trabalho printMessage("Saída Síncrona") // Dá tempo para a goroutine terminar time.Sleep(1 * time.Second)}É importante lembrar: a função principal main também é uma goroutine. Se ela terminar, todas as outras goroutines serão destruídas. Por isso, frequentemente se usa sync.WaitGroup ou channels para sincronização.
2. Channels: Transferência Segura de Dados Entre Goroutines
Channels são "tubos" tipados através dos quais as goroutines podem trocar dados. O princípio básico é: "Não se comunique compartilhando memória; compartilhe memória comunicando-se."
Criação e uso de um channel:
package main
import "fmt"
func main() { // Cria um channel não bufferizado para int ch := make(chan int) // Inicia uma goroutine que envia dados go func() { for i := 0; i < 5; i++ { ch <- i * 2 } close(ch) // Fecha o channel após enviar todos os dados }() // Lê dados do channel for value := range ch { fmt.Println("Recebido:", value) }}Channels Bufferizados permitem enviar dados sem recebimento imediato:
ch := make(chan string, 3) // Buffer para 3 elementosch <- "A"ch <- "B"ch <- "C"// Agora pode lerfmt.Println(<-ch) // Afmt.Println(<-ch) // B3. Operador select: Trabalhando com Múltiplos Channels
O operador select permite que uma goroutine aguarde várias operações de channel simultaneamente. Ele bloqueia até que um dos seus casos esteja pronto para execução.
Exemplo com timeout e processamento de múltiplas fontes de dados:
package main
import ( "fmt" "time")
func worker(id int, ch chan string) { time.Sleep(time.Duration(id) * time.Second) ch <- fmt.Sprintf("Resultado do worker %d", id)}
func main() { ch1 := make(chan string) ch2 := make(chan string) go worker(2, ch1) // Termina em 2 seg go worker(3, ch2) // Termina em 3 seg select { case msg1 := <-ch1: fmt.Println("Primeiro channel:", msg1) case msg2 := <-ch2: fmt.Println("Segundo channel:", msg2) case <-time.After(1 * time.Second): fmt.Println("Timeout! Nenhum worker conseguiu") } // Fecha os channels para evitar vazamentos close(ch1) close(ch2)}O operador select é ideal para implementar timeouts, operações não bloqueantes e o padrão "fan-in" (combinação de vários channels em um).
4. Padrões de Concorrência em Go
Vamos considerar