Introdução: Por que a programação assíncrona em Go?
Go (Golang) foi projetado desde o início para trabalhar eficientemente com concorrência. Ao contrário de muitas linguagens onde a assincronicidade é implementada através de callbacks complexos ou promises, Go oferece um modelo elegante baseado em goroutines e channels. Este modelo permite escrever código assíncrono que é lido como síncrono, mas executado paralelamente, maximizando a utilização dos recursos de processadores multi-core.
Neste guia, abordaremos todos os conceitos-chave da programação assíncrona em Go: desde a criação de goroutines até padrões complexos de sincronização usando channels e primitivas do pacote sync. O material será útil tanto para iniciantes que estão aprendendo Go quanto para desenvolvedores experientes que desejam sistematizar seus conhecimentos.
1. Goroutines: Threads leves
Uma goroutine é uma thread de execução leve, gerenciada pelo runtime do Go. Ao contrário das threads do SO, as goroutines têm uma pilha minúscula (começando com alguns KB) que pode crescer e encolher dinamicamente. Isso permite executar dezenas de milhares de goroutines simultaneamente sem consumo significativo de memória.
Iniciando uma goroutine
Para executar uma função como goroutine, basta adicionar a palavra-chave go antes da chamada da função:
package main
import ( "fmt" "time")
func printNumbers() { for i := 1; i <= 5; i++ { fmt.Println(i) time.Sleep(100 * time.Millisecond) }}
func main() { // Executa a função como uma goroutine go printNumbers()
// A goroutine principal continua trabalhando fmt.Println("Goroutine principal está trabalhando")
// Dá tempo para a goroutine filha executar time.Sleep(1 * time.Second) fmt.Println("Programa finalizado")}Importante: O programa termina quando a função main() termina. Se a goroutine principal terminar antes, todas as goroutines filhas serão interrompidas forçadamente. Por isso, no exemplo, usamos time.Sleep para esperar — na prática, para sincronização, usam-se channels ou sync.WaitGroup.
Funções anônimas em goroutines
Goroutines são frequentemente criadas com funções anônimas:
go func() { fmt.Println("Executando em uma goroutine")}()2. Channels: Comunicação entre goroutines
Channels são "tubos" tipados através dos quais as goroutines podem enviar e receber valores. Este é o principal mecanismo de sincronização e troca de dados em Go. Os channels garantem acesso seguro aos dados sem o uso de locks explícitos (mutexes).
Criação e uso de um channel
Um channel é criado com a função make(). O operador <- é usado para enviar e receber dados:
package main
import "fmt"
func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // envia o resultado para o channel}
func main() { s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
x, y := <-c, <-c // recebe dados do channel
fmt.Println(x, y, x+y)}Neste exemplo, iniciamos duas goroutines, cada uma calcula a soma de metade do slice. Os resultados são enviados para o channel, e a goroutine principal os recebe e exibe a soma total.
Channels com buffer
Por padrão, os channels não têm buffer — o envio é bloqueado até que o receptor esteja pronto para receber o valor. Channels com buffer têm uma fila de tamanho definido:
ch := make(chan int, 2) // channel com buffer para 2 elementosch <- 1ch <- 2// ch <- 3 // erro: deadlock, buffer cheiofmt.Println(<-ch) // 1fmt.Println(<-ch) // 2