data race detected em Go
Uma data race (condição de corrida de dados) ocorre quando duas ou mais goroutines acessam a mesma variável de memória simultaneamente, e pelo menos uma delas está escrevendo. O Go possui um race detector integrado que detecta essas condições em tempo de execução, emitindo o aviso “WARNING: DATA RACE” e detalhando exatamente onde o acesso concorrente acontece.
Data races são bugs sérios: podem causar corrupção de dados, panics inesperados e comportamentos não determinísticos que são extremamente difíceis de reproduzir e debugar.
A Mensagem de Erro
==================
WARNING: DATA RACE
Read at 0x00c0000a4000 by goroutine 7:
main.main.func1()
/app/main.go:15 +0x3e
Previous write at 0x00c0000a4000 by goroutine 8:
main.main.func2()
/app/main.go:21 +0x4e
Goroutine 7 (running) created at:
main.main()
/app/main.go:13 +0x8e
Goroutine 8 (running) created at:
main.main()
/app/main.go:19 +0xb8
==================
Found 1 data race(s)
exit status 66
Como Usar o Race Detector
O race detector é ativado com a flag -race:
# Executar com race detector
go run -race .
# Testar com race detector
go test -race ./...
# Build com race detector (para ambientes de staging)
go build -race -o app .
O race detector adiciona overhead de CPU (~2-10x mais lento) e memória (~5-10x mais memória), então use em desenvolvimento e testes, não em produção.
Causas Comuns
1. Variável Compartilhada Sem Proteção
O caso clássico — múltiplas goroutines acessam a mesma variável:
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()
// DATA RACE: leitura e escrita concorrentes
contador++
}()
}
wg.Wait()
// Resultado imprevisível (pode ser 980, 995, 1000...)
fmt.Println(contador)
}
2. Map Concorrente
Maps em Go não são thread-safe:
package main
import "sync"
func main() {
m := make(map[string]int)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// DATA RACE: escrita concorrente em map
// Pode causar: "fatal error: concurrent map writes"
m[fmt.Sprintf("key-%d", n)] = n
}(i)
}
wg.Wait()
}
3. Slice Compartilhado
Slices com append concorrente:
package main
import (
"fmt"
"sync"
)
func main() {
var resultados []int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// DATA RACE: append concorrente modifica o slice header
resultados = append(resultados, n)
}(i)
}
wg.Wait()
fmt.Println(len(resultados)) // Resultado imprevisível
}
4. Struct com Campos Acessados por Múltiplas Goroutines
package main
import (
"sync"
)
type Server struct {
connections int
running bool
}
func main() {
s := &Server{}
var wg sync.WaitGroup
// Goroutine 1: modifica connections
wg.Add(1)
go func() {
defer wg.Done()
s.connections++ // DATA RACE
}()
// Goroutine 2: lê connections
wg.Add(1)
go func() {
defer wg.Done()
_ = s.connections // DATA RACE
}()
wg.Wait()
}
Como Resolver
Solução 1: sync.Mutex
O mutex (mutual exclusion) garante que apenas uma goroutine acessa o recurso por vez:
package main
import (
"fmt"
"sync"
)
func main() {
contador := 0
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
contador++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(contador) // Sempre 1000
}
Para leituras frequentes e escritas raras, use sync.RWMutex:
var mu sync.RWMutex
// Leitura — múltiplas goroutines podem ler simultaneamente
mu.RLock()
valor := mapa["chave"]
mu.RUnlock()
// Escrita — exclusiva, bloqueia todas as leituras
mu.Lock()
mapa["chave"] = 42
mu.Unlock()
Solução 2: sync/atomic
Para operações simples em inteiros e ponteiros, use operações atômicas:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var contador atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contador.Add(1) // Operação atômica — sem data race
}()
}
wg.Wait()
fmt.Println(contador.Load()) // Sempre 1000
}
Solução 3: sync.Map para Maps Concorrentes
Use sync.Map quando múltiplas goroutines precisam acessar um map:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m.Store(fmt.Sprintf("key-%d", n), n)
}(i)
}
wg.Wait()
// Leitura segura
m.Range(func(key, value any) bool {
fmt.Printf("%s: %v\n", key, value)
return true
})
}
Solução 4: Channels (Comunicação ao Invés de Compartilhamento)
O provérbio Go diz: “Don’t communicate by sharing memory; share memory by communicating.”
package main
import "fmt"
func main() {
resultados := make(chan int, 100)
done := make(chan bool)
// Produtor: goroutines enviam resultados pelo channel
for i := 0; i < 100; i++ {
go func(n int) {
resultados <- n * 2
}(i)
}
// Consumidor: uma única goroutine agrega
go func() {
var soma int
for i := 0; i < 100; i++ {
soma += <-resultados
}
fmt.Println("Soma:", soma)
done <- true
}()
<-done
}
Veja mais sobre este padrão em concorrência em Go e padrões de concorrência.
Solução 5: Confinar Dados a Uma Goroutine
Às vezes a melhor solução é não compartilhar:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
resultados := make([]int, 100) // Pre-alocado
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// Cada goroutine acessa apenas SEU índice
// Sem compartilhamento = sem data race
resultados[idx] = idx * 2
}(i)
}
wg.Wait()
fmt.Println(resultados[:5]) // [0 2 4 6 8]
}
Integrando o Race Detector no CI/CD
Adicione o race detector ao seu pipeline de testes e CI/CD:
# .github/workflows/test.yml
- name: Run tests with race detector
run: go test -race -count=5 ./...
Usar -count=5 executa cada teste 5 vezes, aumentando a chance de expor race conditions intermitentes.
Dicas para Evitar Data Races
Execute testes com
-racesempre — integre ao CI. Veja TDD e CI/CD em Go.Prefira channels a mutexes — channels deixam o fluxo de dados explícito. Leia sobre concorrência em Go.
Use
sync/atomicpara contadores — mais eficiente que mutex para operações simples.Imutabilidade — passe cópias ao invés de ponteiros quando possível.
Evite goroutines que compartilham estado — confine dados quando possível. Consulte padrões de concorrência.
Use observabilidade — monitore goroutines e contenção de locks em produção com pprof e profiling.
Curiosamente, Rust elimina data races completamente em tempo de compilação graças ao ownership — o compilador garante que dados mutáveis nunca são compartilhados entre threads sem sincronização explícita.
Erros Relacionados
- deadlock — all goroutines are asleep — quando sincronização excessiva bloqueia tudo
- nil pointer dereference — data races podem causar ponteiros nil inesperados
- Concorrência em Go — fundamentos de goroutines e channels
- Padrões de Concorrência — fan-out, fan-in, pipeline, worker pool