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

  1. Execute testes com -race sempre — integre ao CI. Veja TDD e CI/CD em Go.

  2. Prefira channels a mutexes — channels deixam o fluxo de dados explícito. Leia sobre concorrência em Go.

  3. Use sync/atomic para contadores — mais eficiente que mutex para operações simples.

  4. Imutabilidade — passe cópias ao invés de ponteiros quando possível.

  5. Evite goroutines que compartilham estado — confine dados quando possível. Consulte padrões de concorrência.

  6. 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