O que é concorrência em Go
Go foi projetado desde o início para lidar com concorrência de forma nativa e simples. Criado pelo Google em 2009 e lançado oficialmente em 2012, Go nasceu para resolver um problema real: escrever servidores que atendem milhares de conexões simultâneas sem complicar o código.
A proposta central do Go é que concorrência não precisa ser difícil. Enquanto outras linguagens exigem threads pesadas, callbacks aninhados ou frameworks complexos, Go oferece goroutines, que são funções que rodam de forma concorrente com um custo mínimo de memória (cerca de 2 KB no início).
O modelo de Go é baseado na filosofia CSP (Communicating Sequential Processes), criada por Tony Hoare nos anos 70. A ideia é simples: em vez de compartilhar memória e usar locks para proteger, você passa dados através de channels. Menos bugs, código mais claro.
Como funciona a concorrência em Go
O runtime do Go gerência um conjunto de threads do sistema operacional e distribui goroutines entre elas automaticamente. Você não precisa criar ou gerenciar threads manualmente. O scheduler do Go usa um modelo M:N, onde M goroutines rodam em N threads do SÓ.
Quando uma goroutine faz uma operação de I/O ou bloqueia em um channel, o scheduler a suspende e coloca outra goroutine no lugar. Isso permite que você tenha milhares de goroutines rodando sem esgotar os recursos do sistema.
Os channels são filas tipadas e thread-safe usadas para comunicação entre goroutines. Um channel pode ser buffered (com capacidade definida) ou unbuffered (síncrono, onde sender e receiver precisam estar prontos ao mesmo tempo).
A máxima do Go para concorrência é: "Do not communicate by sharing memory; instead, share memory by communicating." Lembre disso sempre que for usar um mutex onde um channel resolveria melhor.
Principais recursos de concorrência
Go oferece um conjunto enxuto mas poderoso de primitivas de concorrência:
- Goroutines: funções leves iniciadas com a palavra-chave
go. Custam muito menos que threads do SÓ e você pode ter facilmente centenas de milhares delas. - Channels: canais tipados para comunicação entre goroutines. Podem ser bidirecionais ou unidirecionais (
chan<-só envia,<-chansó recebe). - select: permite esperar em múltiplos channels ao mesmo tempo, similar ao switch mas para operações de I/O concorrentes.
- sync.WaitGroup: aguarda um grupo de goroutines terminar antes de prosseguir.
- sync.Mutex e sync.RWMutex: para quando você realmente precisa proteger acesso a memória compartilhada.
- context: para cancelamento e deadline de operações concorrentes.
O diferencial do Go em relação a outras linguagens é a leveza das goroutines e a integração nativa dos channels na sintaxe. Não é uma biblioteca adicional, é parte do núcleo da linguagem.
Como começar: instalação e primeiro exemplo
Se você ainda não tem Go instalado, o processo é direto:
# Baixar o instalador em https://go.dev/dl/
# Verificar a instalação:
go version
# Deve mostrar algo como: go version go1.22.0 Linux/amd64Crie um arquivo main.go e escreva sua primeira goroutine:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d iniciando\n", id)
// simula trabalho
fmt.Printf("Worker %d concluído\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("Todos os workers concluídos")
}Sem o wg.Wait(), o programa principal pode terminar antes das goroutines concluírem. Sempre sincronize o fim das goroutines que você lançar.
Execute com go run main.go e observe que a ordem de saída pode variar a cada execução - isso é concorrência real funcionando.
Exemplo prático: pipeline com channels
Um dos padrões mais elegantes em Go é o pipeline: goroutines conectadas por channels onde cada uma processa e passa os dados para a próxima.
package main
import "fmt"
// Gera números e envia pelo channel
func gerador(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Eleva ao quadrado cada número recebido
func quadrado(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Montar o pipeline
c := gerador(2, 3, 4, 5)
out := quadrado(c)
// Consumir resultado
for v := range out {
fmt.Println(v) // 4, 9, 16, 25
}
}Note como cada estágio do pipeline é uma função que retorna um channel de leitura. O chamador não precisa saber como os dados são produzidos, só lê do channel. Essa separação de responsabilidades torna o código muito mais testável e reutilizável.
Comparação com alternativas
Cada linguagem tem sua abordagem para concorrência:
Python (asyncio): concorrência cooperativa com async/await. Ótimo para I/O, mas ainda single-threaded por causa do GIL. Para CPU-bound, precisa de multiprocessing. Go usa threads reais e não tem essa limitação.
Java (threads/CompletableFuture): threads do SÓ são pesadas (cerca de 1 MB por thread). Com Java 21 virtual threads melhorou muito, mas a API ainda é mais verbosa que Go. Go tem goroutines com 2 KB iniciais.
Node.js: event loop single-threaded com callbacks/Promises. Excelente para I/O mas código assíncrono aninhado pode virar spaghetti. Go é imperativo e linear mesmo quando concorrente.
Quando usar Go: servidores com alta concorrência, processamento paralelo de dados, ferramentas de linha de comando que fazem muitas coisas ao mesmo tempo. Não é a melhor escolha para scripts simples ou quando o time não tem familiaridade com o modelo de concorrência.
Pontos positivos e limitações
Pontos positivos: goroutines são extremamente leves e baratas. O modelo de channels elimina uma classe inteira de bugs de race condition. O detector de race conditions embutido (go run -race) é uma ferramenta valiosa. A simplicidade da API torna o código concorrente legível.
Limitações reais: channels não são a bala de prata para tudo. Para casos de hot path com muita contenção, um mutex pode ser mais performático. O garbage collector pode pausar goroutines em momentos críticos, então Go não é ideal para sistemas de tempo real hard. Também não existe um equivalente direto a Promise.all fácil de usar sem WaitGroup ou errgroup.
Goroutine leak é um problema real. Se você lançar uma goroutine que fica bloqueada em um channel que nunca será fechado ou enviado, ela vive para sempre consumindo memória. Sempre planeje como suas goroutines vão terminar.
Casos de uso reais
Backend de API com alta concorrência: um servidor Go lida com cada request em uma goroutine separada. Com o pacote net/http padrão, você já tem um servidor concorrente sem configuração extra. Empresas como Cloudflare, Uber e Twitch usam Go exatamente por isso.
Processamento de filas: consumir mensagens de uma fila (Kafka, RabbitMQ, SQS) com workers paralelos é trivial em Go. Você lança N goroutines, cada uma lendo do mesmo channel, e o próprio Go distribui o trabalho.
Ferramentas de CI/CD e DevOps: Kubernetes, Docker, Terraform, e a maioria das ferramentas modernas de infra são escritas em Go. O modelo de concorrência facilita operações paralelas em clusters ou múltiplos recursos.
Web scraping e automação: fazer centenas de requisições HTTP em paralelo com rate limiting controlado é um padrão clássico em Go, usando semaphore pattern com channels buffered.
Dicas e boas práticas
Sempre feche channels pelo lado do sender, nunca pelo receiver. E sempre use range para iterar channels - ele para automaticamente quando o channel é fechado.
Use o pacote golang.org/x/sync/errgroup em vez de WaitGroup quando suas goroutines podem retornar erros. Ele propaga o primeiro erro e cancela as demais automaticamente via context.
Rode sempre com go test -race ./... no seu CI. O race detector do Go encontra condições de corrida que testes normais nunca pegariam, sem custo de escrever código extra.
Outro padrão importante: use context.WithTimeout ou context.WithCancel para garantir que operações longas possam ser canceladas. Propague o context como primeiro argumento de todas as funções que fazem I/O ou lançam goroutines.
Vale a pena aprender?
Para devs que trabalham com backend, automação ou ferramentas de infra: sim, definitivamente. Go é uma das linguagens com melhor custo-benefício de aprendizado no mercado hoje. A curva inicial é suave, a concorrência é o ponto mais difícil, mas com os padrões deste guia você já tem 80% do que precisa para o dia a dia.
Se você já conhece Python ou Java, vai se sentir em casa com a sintaxe em menos de uma semana. O modelo de concorrência vai levar um pouco mais para internalizar, mas vale o esforço. Comece pelo tour oficial em go.dev/tour e depois pratique com projetos reais.
Comentários
Deixar um comentárioVocê precisa ter uma conta no CuritibaBlog para comentar.