Concorrência em Go: Goroutines e Channels
Resposta rápida: em Go, goroutines são unidades leves de concorrência (go f()), channels transportam valores entre elas e select multiplexa waits. Junte sync.WaitGroup ou errgroup, context.Context para cancelar e go test -race para caçar corridas. Com isso você cobre o núcleo pedido em APIs, workers e entrevistas — sem framework.
Este guia é o ponto de entrada prático de concorrência no golang.com.br: o modelo mental, exemplos que compilam, armadilhas (leak, race, close errado) e o caminho para os guias de produção — channels, errgroup, worker pool, sync.Mutex / WaitGroup / RWMutex / Once e testes com -race.
Goroutines vs threads vs async (visão rápida)
| Modelo | Custo típico | Quem agenda | Encaixa bem quando |
|---|---|---|---|
| Thread OS | ~1 MB stack, create caro | Kernel | Poucas tarefas bloqueantes nativas |
| Goroutine (Go) | ~2 KB stack inicial, create barato | Runtime Go (M:N) | APIs, I/O, fan-out, workers |
| Async/await (outras langs) | Stacks lógicas / futures | Event loop + runtime | Ecossistemas já async-first |
Go não exige anotar cada chamada com async. Você marca o ponto de concorrência com go e usa channels/context para coordenar. Para comparar com ownership/Tokio, veja Go vs Rust; para o mercado backend, Go para backend.
Goroutines
Uma goroutine é uma função executando concorrentemente com outras. Para criar, use a palavra-chave go:
package main
import (
"fmt"
"time"
)
func dizer(msg string) {
for i := 0; i < 3; i++ {
fmt.Println(msg)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go dizer("goroutine") // executa concorrentemente
dizer("main") // executa na goroutine principal
time.Sleep(500 * time.Millisecond)
}
Características das goroutines
- Leves: stack inicial pequena (~2 KB), cresce conforme a necessidade
- Multiplexadas: milhares podem rodar em poucas threads OS
- Gerenciadas pelo runtime: o scheduler do Go troca de contexto; desde versões recentes há preempção mais agressiva em loops longos
// Criar muitas goroutines é barato — mas cada uma ainda precisa terminar
for i := 0; i < 1000; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
Armadilha de captura de loop: antes do Go 1.22, o i do for era reutilizado e todas as goroutines podiam ver o mesmo valor. Em Go 1.22+ cada iteração tem sua própria variável. Mesmo assim, passar n int como argumento continua sendo o estilo mais explícito e seguro em reviews.
Channels
Channels são a forma idiomática de comunicação entre goroutines. O ditado oficial:
“Não comunique compartilhando memória; compartilhe memória comunicando.”
Criando e usando channels
ch := make(chan int)
go func() {
ch <- 42 // envia
}()
valor := <-ch // recebe
fmt.Println(valor) // 42
Channels bufferizados
ch := make(chan int, 3)
ch <- 1 // não bloqueia
ch <- 2
ch <- 3
// ch <- 4 // bloquearia: buffer cheio
Buffer não “resolve” race condition: só muda quando o send bloqueia. Use buffer para suavizar rajadas (jobs, logs), não como desculpa para não desenhar backpressure.
Direção de channels
func produtor(ch chan<- int) { ch <- 1 }
func consumidor(ch <-chan int) {
valor := <-ch
_ = valor
}
Restringir a direção no tipo da função documenta o contrato e evita close acidental no consumidor.
Fechando channels
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // só o produtor fecha
}()
for v := range ch {
fmt.Println(v)
}
valor, ok := <-ch
if !ok {
fmt.Println("channel fechado")
}
Regra: quem envia fecha. Nunca feche do lado receptor. Fechar duas vezes ou enviar em channel fechado gera panic.
Select
select espera o primeiro case pronto:
select {
case msg1 := <-ch1:
fmt.Println("ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("ch2:", msg2)
case ch3 <- valor:
fmt.Println("enviou para ch3")
default:
fmt.Println("nenhum channel pronto")
}
Timeout e cancelamento
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
Em produção, prefira context a time.After solto em loops quentes (cada After aloca um timer). Padrão idiomático:
select {
case res := <-ch:
return res, nil
case <-ctx.Done():
return 0, ctx.Err()
}
O guia context.Context: timeout e cancelamento aprofunda deadlines em HTTP, DB e workers.
sync.WaitGroup (e quando subir para errgroup)
Para esperar N goroutines terminarem sem channel de resultados:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
Chame Add antes de go. Done no defer. Para fan-out com cancelamento no primeiro erro, troque o WaitGroup manual por errgroup. Para o primer completo de Mutex, RWMutex, Once e WaitGroup, use o guia de sync.
Padrões comuns
Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d processando job %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
<-results
}
}
Em produção você ainda precisa de: context, tamanho de fila, métricas, graceful shutdown e teste sem time.Sleep. O deep dive está em Worker Pool em Go. Para filas externas (SQS, Redis Streams, cron), veja também SQS workers e cron/jobs agendados.
Fan-out / Fan-in
func fanIn(inputs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, in := range inputs {
wg.Add(1)
go func(ch <-chan int) {
defer wg.Done()
for v := range ch {
out <- v
}
}(in)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Quando o fan-out chama APIs e deve cancelar tudo no primeiro erro, errgroup.WithContext costuma ser mais claro que WaitGroup + channel de erro manual.
Pipeline
func gerador(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func quadrado(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func main() {
for v := range quadrado(gerador(1, 2, 3, 4)) {
fmt.Println(v) // 1, 4, 9, 16
}
}
Pipelines brilham em ETL, parsers e processamento em estágios. Cada estágio fecha o próprio output; o estágio seguinte faz range.
Deduplicar trabalho repetido (cache stampede)
Se N goroutines disparam o mesmo fetch caro ao mesmo tempo, singleflight deduplica a chamada. Não substitui channel — complementa cache. Veja singleflight e cache stampede.
Context: o fio que segura produção
Quase todo serviço Go real passa ctx adentro:
- Handler HTTP recebe
r.Context() - Propaga para DB, HTTP client e workers
- No shutdown, cancela o contexto raiz (veja graceful shutdown)
Checklist mínimo em código concorrente:
- Toda goroutine de longa duração observa
ctx.Done() - Timeouts usam
context.WithTimeout/WithDeadline - Não guarde
Contextem structs por tempo indefinido sem motivo claro - Cancele ao retornar erro — não deixe fan-out órfão
Armadilhas comuns
1. Race condition
// ERRADO
counter := 0
for i := 0; i < 1000; i++ {
go func() { counter++ }()
}
// CORRETO — mutex (ou atomic.Int64)
var mu sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
2. Goroutine leak
// ERRADO — ninguém envia, ninguém cancela
func leak() {
ch := make(chan int)
go func() {
<-ch
}()
}
// CORRETO
func semLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case <-ch:
case <-ctx.Done():
return
}
}()
}
3. Fechar channel do lado errado
Só o produtor fecha. Consumidores fazem range ou v, ok := <-ch.
4. Send sem buffer e sem receptor
Send em channel unbuffered bloqueia até alguém receber. Em shutdown, isso vira deadlock se o receptor já saiu. Prefira select com ctx.Done() ou buffer dimensionado + abandono explícito.
Detectando race conditions
go test -race ./...
go build -race -o app ./cmd/api
O detector só vê caminhos exercitados. Combine com table-driven tests, flags de go test (-count, -shuffle) e, em hot paths, pprof / go tool trace para ver stalls e goroutines presas.
Checklist de produção (concorrência)
| Item | Por quê |
|---|---|
context em bordas (HTTP, jobs) | Cancelamento e timeout únicos |
Limite de fan-out (errgroup.SetLimit / worker pool) | Evita abrir 10k conexões |
| Backpressure (buffer finito + bloqueio) | Protege memória |
-race no CI de pacotes críticos | Pega data races cedo |
| Métricas: fila, workers busy, tempo de wait | Opera o sistema, não só o código |
| Shutdown: pare de aceitar → drena → cancela | Deploy sem matar request no meio |
Concorrência na carreira Go
Em perguntas de entrevista e vagas, cobram menos “o que é goroutine?” e mais:
- Diferença channel vs mutex
- Como você cancela um fan-out
- Como testa race sem flaky sleep
- Quando worker pool local perde para fila (Kafka/SQS)
Monte um projeto pequeno (API + worker) no portfólio e saiba narrar o desenho. Para remuneração e senioridade, cruze com salários Go e plano de carreira.
Recursos oficiais
Para outra lente sobre o mesmo problema, use o comparativo Go vs Rust e os artigos irmãos Rust vs Go no Rust Brasil e structured concurrency em Kotlin.
Última atualização: Agosto 2026
Veja também
- Channels em produção — Semântica fina de send/recv/close
- errgroup — Fan-out com cancelamento
- Worker pool — Filas de jobs na prática
- sync.Mutex e WaitGroup — Quando lock é a ferramenta certa
- context.Context — Timeout e cancelamento
- Testes em Go —
-race, table-driven, CI - WebSocket com Go — Concorrência em tempo real
- Microservices com Go — Serviços e fan-out
- Tratamento de erros — Erros em código concorrente
- Cheatsheet — Referência rápida
- Vagas Go — Onde a skill aparece no mercado