Concorrência bem implementada torna sistemas mais rápidos, mais responsivos e mais eficientes. Concorrência mal implementada produz bugs dos mais difíceis de diagnosticar na engenharia de software — bugs que aparecem intermitentemente, que somem quando você adiciona um log para investigá-los, que só se manifestam sob carga específica em produção.
Este artigo cataloga as armadilhas mais comuns da concorrência em Go, mostra como cada uma se manifesta, e apresenta as ferramentas e técnicas para detectá-las e corrigi-las.
Race condition: o bug mais comum
Uma race condition ocorre quando o resultado de um programa depende da ordem de execução de goroutines, e essa ordem não está garantida. O exemplo clássico é o incremento de um contador compartilhado sem sincronização:
package main
import (
"fmt"
"sync"
)
func main() {
contador := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contador++ // RACE CONDITION — leitura + incremento + escrita não são atômicas
}()
}
wg.Wait()
fmt.Println(contador) // raramente 1000 — valor imprevisível
}
O que parece uma operação simples — contador++ — é na verdade três operações:
- Ler o valor atual de
contador - Incrementar o valor lido
- Escrever o novo valor
Se duas goroutines executam esse ciclo simultaneamente, ambas podem ler o mesmo valor, incrementar para o mesmo resultado e escrever o mesmo valor — perdendo um incremento.
Correção com Mutex:
type ContadorSeguro struct {
mu sync.Mutex
valor int
}
func (c *ContadorSeguro) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock()
c.valor++
}
func (c *ContadorSeguro) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
Correção com atomic:
Para operações simples em tipos escalares, o pacote sync/atomic oferece operações atômicas sem o overhead de um mutex:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var contador int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&contador, 1) // atômico — seguro sem mutex
}()
}
wg.Wait()
fmt.Println(atomic.LoadInt64(&contador)) // sempre 1000
}
O detector de race conditions
Go inclui um detector de race conditions integrado ao toolchain. Ele instrumenta o binário para registrar todos os acessos à memória e detectar acessos concorrentes não sincronizados:
go test -race ./...
go run -race main.go
go build -race -o app ./...
O detector produz relatórios detalhados quando detecta uma race:
==================
WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 7:
main.main.func1()
/home/usuario/projeto/main.go:14 +0x3c
Previous read at 0x00c0000b4010 by goroutine 6:
main.main.func1()
/home/usuario/projeto/main.go:14 +0x2e
Goroutine 7 (running) created at:
main.main()
/home/usuario/projeto/main.go:12 +0x74
==================
O relatório indica exatamente onde a race ocorreu: arquivo, linha, goroutine envolvida e onde a goroutine foi criada. Com essa informação, a correção é direta.
O detector tem custo — programas instrumentados executam entre 5 e 20 vezes mais lentamente e consomem mais memória. Use em desenvolvimento e CI, nunca em binários de produção.
Deadlock: travamento mútuo
Um deadlock ocorre quando duas ou mais goroutines esperam indefinidamente umas pelas outras. Go detecta deadlocks totais — quando todas as goroutines estão bloqueadas — e encerra o programa com uma mensagem diagnóstica:
package main
import "sync"
func main() {
var mu1, mu2 sync.Mutex
// Goroutine A: trava mu1, depois tenta travar mu2
go func() {
mu1.Lock()
defer mu1.Unlock()
// ... faz algo
mu2.Lock() // espera mu2 ser liberado pela goroutine B
defer mu2.Unlock()
}()
// Goroutine B: trava mu2, depois tenta travar mu1
mu2.Lock()
defer mu2.Unlock()
// ... faz algo
mu1.Lock() // espera mu1 ser liberado pela goroutine A
defer mu1.Unlock()
// Deadlock: A espera B, B espera A
}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
...
goroutine 6 [semacquire]:
...
Prevenção: estabeleça uma ordem global de aquisição de locks e sempre adquira-os nessa ordem em todas as goroutines:
// Convenção: sempre travar mu1 antes de mu2
func operacao() {
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// seção crítica
}
Livelock: movimento sem progresso
Um livelock é similar a um deadlock, mas as goroutines não estão bloqueadas — estão ativas, respondendo umas às outras, mas sem progredir:
package main
import (
"fmt"
"sync"
"time"
)
type Agente struct {
nome string
mu sync.Mutex
ocupado bool
}
func (a *Agente) tentarTrabalhar(outro *Agente) {
for {
a.mu.Lock()
if a.ocupado {
a.mu.Unlock()
fmt.Printf("%s: esperando o outro terminar\n", a.nome)
time.Sleep(1 * time.Millisecond)
continue
}
outro.mu.Lock()
if outro.ocupado {
outro.mu.Unlock()
a.mu.Unlock()
fmt.Printf("%s: cortesia — cedendo ao outro\n", a.nome)
time.Sleep(1 * time.Millisecond)
continue
}
fmt.Printf("%s: trabalhando!\n", a.nome)
a.mu.Unlock()
outro.mu.Unlock()
return
}
}
func main() {
agente1 := &Agente{nome: "A"}
agente2 := &Agente{nome: "B"}
// Ambos ficam cedendo um ao outro indefinidamente
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); agente1.tentarTrabalhar(agente2) }()
go func() { defer wg.Done(); agente2.tentarTrabalhar(agente1) }()
// wg.Wait() — ficaria esperando para sempre
time.Sleep(100 * time.Millisecond)
fmt.Println("livelock demonstrado")
}
Livelocks são difíceis de detectar porque o programa parece estar executando normalmente. A solução geralmente envolve aleatoriedade no tempo de espera — backoff exponencial com jitter — para que as goroutines não entrem em ciclos sincronizados.
Starvation: inanição de goroutines
Starvation ocorre quando uma goroutine nunca consegue acesso a um recurso compartilhado porque outras goroutines monopolizam o acesso:
package main
import (
"fmt"
"sync"
"time"
)
type RecursoCompartilhado struct {
mu sync.Mutex
}
func trabalhoIntensivo(r *RecursoCompartilhado, nome string, duracao time.Duration) {
for i := 0; i < 5; i++ {
r.mu.Lock()
fmt.Printf("%s: usando recurso (iteração %d)\n", nome, i)
time.Sleep(duracao)
r.mu.Unlock()
// goroutine libera e tenta readquirir imediatamente
// goroutines de baixa prioridade raramente conseguem o lock
}
}
func main() {
r := &RecursoCompartilhado{}
// Goroutines "rápidas" monopolizam o recurso
go trabalhoIntensivo(r, "rápido-1", 10*time.Millisecond)
go trabalhoIntensivo(r, "rápido-2", 10*time.Millisecond)
// Goroutine "lenta" pode ficar em starvation
go trabalhoIntensivo(r, "lento", 50*time.Millisecond)
time.Sleep(2 * time.Second)
}
Para mitigar starvation, considere sync.RWMutex para leituras concorrentes, filas de trabalho com distribuição justa ou o pacote golang.org/x/sync/semaphore que implementa filas de espera FIFO.
Goroutine leak: o vazamento silencioso
Como discutido no Artigo 19, goroutines que nunca terminam são um problema sério. Os casos mais comuns:
Channel sem receptor:
func vazamento1() {
ch := make(chan int)
go func() {
resultado := calcularAlgo()
ch <- resultado // bloqueia para sempre — ninguém lê
}()
// função retorna sem ler ch
}
Goroutine esperando channel que nunca será fechado:
func vazamento2() {
ch := make(chan struct{})
go func() {
<-ch // bloqueia para sempre se ch nunca for fechado
fmt.Println("nunca executado")
}()
// ch nunca é fechado
}
Goroutine em loop infinito sem saída:
func vazamento3(ctx context.Context) {
go func() {
for {
// processamento
time.Sleep(1 * time.Second)
// sem verificação de ctx.Done() — nunca termina
}
}()
}
Detecção com goleak:
package minhasoperacoes_test
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestOperacao(t *testing.T) {
defer goleak.VerifyNone(t)
operacaoQuePodeMinar()
}
Detecção manual com runtime:
func monitorarGoroutines(t *testing.T) func() {
antes := runtime.NumGoroutine()
return func() {
time.Sleep(10 * time.Millisecond) // dá tempo para goroutines terminarem
depois := runtime.NumGoroutine()
if depois > antes {
t.Errorf("possível goroutine leak: antes=%d depois=%d", antes, depois)
}
}
}
func TestSemVazamento(t *testing.T) {
verificar := monitorarGoroutines(t)
defer verificar()
// ... código do teste
}
O problema da variável de loop capturada
Um dos bugs mais clássicos em Go — tão comum que o Go 1.22 mudou o comportamento padrão para corrigi-lo:
// Go 1.21 e anteriores — BUG
func main() {
var wg sync.WaitGroup
valores := []string{"a", "b", "c"}
for _, v := range valores {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v) // captura v por referência — todas imprimem "c"
}()
}
wg.Wait()
}
// Correção para Go 1.21 e anteriores
for _, v := range valores {
v := v // cria nova variável local a cada iteração
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v) // captura a cópia local
}()
}
// Go 1.22+ — comportamento correto por padrão
// Cada iteração cria automaticamente uma nova variável v
Uso incorreto de sync/atomic
Operações atômicas garantem atomicidade individual, mas não compõem automaticamente:
var (
leituras int64
escritas int64
)
// ERRADO — as duas operações não são atômicas em conjunto
func estatisticas() (int64, int64) {
l := atomic.LoadInt64(&leituras)
e := atomic.LoadInt64(&escritas)
// outro goroutine pode modificar entre as duas leituras
return l, e
}
// CORRETO — usar mutex para operações compostas
var mu sync.Mutex
func estatisticasSeguras() (int64, int64) {
mu.Lock()
defer mu.Unlock()
return leituras, escritas
}
Map concorrente sem proteção
// ERRADO — map não é thread-safe
m := make(map[string]int)
go func() { m["a"] = 1 }()
go func() { m["b"] = 2 }()
// Panic: concurrent map writes
// CORRETO — opção 1: RWMutex
type MapaSeguro struct {
mu sync.RWMutex
dados map[string]int
}
// CORRETO — opção 2: sync.Map para padrões específicos
var m sync.Map
go func() { m.Store("a", 1) }()
go func() { m.Store("b", 2) }()
Checklist de revisão de código concorrente
Ao revisar código que usa goroutines, use esta lista como guia:
□ Toda goroutine tem uma condição clara de encerramento?
□ Channels são fechados apenas pelo produtor?
□ Channels com goroutines potencialmente bloqueadas têm buffer ou cancelamento?
□ Mutexes nunca são copiados após o primeiro uso?
□ Receivers de métodos com mutex são sempre ponteiros?
□ Não há tentativa de adquirir o mesmo lock duas vezes na mesma goroutine?
□ Operações I/O e lentas estão fora de seções críticas (dentro de locks)?
□ Context é propagado para todas as operações que podem bloquear?
□ Variáveis de loop capturadas em closures são copiadas (Go < 1.22)?
□ O código passa em go test -race?
□ Testes verificam ausência de goroutine leaks?
Ferramentas de diagnóstico
# Detector de race conditions
go test -race ./...
go run -race main.go
# Perfil de goroutines em execução
go tool pprof http://localhost:6060/debug/pprof/goroutine
# Stack trace de todas as goroutines (via SIGQUIT)
kill -SIGQUIT <pid>
# Análise estática
go vet ./...
staticcheck ./...
# Goleak para testes
go get go.uber.org/goleak
O endpoint de profiling requer adicionar o servidor de debug ao programa:
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
Resumo do que foi coberto
Este artigo catalogou as principais armadilhas da concorrência em Go: race conditions com detecção via -race, deadlocks com estratégia de ordem global de locks, livelocks, starvation, goroutine leaks com detecção via goleak, a variável de loop capturada e sua correção no Go 1.22, composição incorreta de operações atômicas, maps concorrentes sem proteção, e um checklist de revisão. Com este artigo, o Módulo 4 de Concorrência está completo.
Referências e leituras complementares
-
Go Race Detector — Documentação oficial do detector de race conditions. https://go.dev/doc/articles/race_detector
-
The Go Memory Model — Especificação formal de como acessos à memória funcionam entre goroutines. https://go.dev/ref/mem
-
goleak — Uber — Biblioteca para detecção de goroutine leaks em testes. https://github.com/uber-go/goleak
-
Go by Example: Atomic Counters — Exemplos práticos de sync/atomic. https://gobyexample.com/atomic-counters
-
Concurrency in Go — Katherine Cox-Buday — Capítulo dedicado a armadilhas e antipadrões. https://www.oreilly.com/library/view/concurrency-in-go/9781491941294/
-
staticcheck — Analisador estático que detecta problemas comuns incluindo uso incorreto de sync. https://staticcheck.dev