Detector de race conditions: go race

1. Introdução às Race Conditions em Go

Race conditions ocorrem quando duas ou mais goroutines acessam a mesma variável simultaneamente, e pelo menos um desses acessos é de escrita. Em Go, o modelo de concorrência baseado em goroutines e canais é poderoso, mas também vulnerável a data races se não for usado corretamente.

var counter int

func main() {
    for i := 0; i < 1000; i++ {
        go func() {
            counter++ // Acesso concorrente sem sincronização
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(counter) // Resultado imprevisível
}

As consequências de data races incluem comportamento indefinido, panics intermitentes e bugs extremamente difíceis de reproduzir. O pior é que esses bugs podem passar despercebidos em testes e só aparecerem em produção sob carga específica.

2. O Detector de Race Conditions: -race Flag

Go oferece um detector de race conditions embutido, ativado pela flag -race. Você pode usá-lo com:

go run -race meuprograma.go
go build -race -o meuprograma
go test -race ./...

O detector funciona instrumentando o código em tempo de compilação, adicionando verificações em todos os acessos a memória compartilhada. Quando um data race é detectado em tempo de execução, o programa imprime um relatório detalhado.

Limitações importantes:
- Overhead de performance: programas com -race podem ser 5-10x mais lentos
- Cobertura apenas em tempo de execução: só detecta races que realmente ocorreram
- Aumento significativo no uso de memória

3. Identificando Data Races com o Output do Detector

Vamos analisar um exemplo prático que demonstra um data race clássico:

package main

import (
    "fmt"
    "sync"
)

type Contador struct {
    valor int
}

func (c *Contador) Incrementar() {
    c.valor++ // Data race: múltiplas goroutines acessam simultaneamente
}

func main() {
    var wg sync.WaitGroup
    contador := &Contador{}

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            contador.Incrementar()
        }()
    }

    wg.Wait()
    fmt.Println("Valor final:", contador.valor)
}

Ao executar com go run -race main.go, você verá um relatório como:

WARNING: DATA RACE
Write at 0x00c0000b4008 by goroutine 8:
  main.(*Contador).Incrementar()
      /path/main.go:12 +0x4f

Previous read at 0x00c0000b4008 by goroutine 7:
  main.(*Contador).Incrementar()
      /path/main.go:12 +0x4f

Goroutine 7 (running) created at:
  main.main()
      /path/main.go:22 +0x7a

O relatório mostra:
- O tipo de acesso (leitura/escrita)
- O endereço de memória envolvido
- As goroutines envolvidas
- As stack traces completas

4. Casos Comuns de Data Races em Go

Acesso concorrente a mapas

func main() {
    m := make(map[int]int)
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m[n] = n * 2 // Data race: map não é thread-safe
        }(i)
    }
    wg.Wait()
}

Escrita em slices compartilhados

func main() {
    results := make([]int, 0, 100)
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            results = append(results, n) // Data race: slice não sincronizado
        }(i)
    }
    wg.Wait()
}

Closures capturadas em loops

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(i) // Data race: variável i é compartilhada entre goroutines
        }()
    }
    wg.Wait()
}

5. Estratégias de Correção com Sincronização

Usando sync.Mutex

type ContadorSeguro struct {
    mu    sync.Mutex
    valor int
}

func (c *ContadorSeguro) Incrementar() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.valor++
}

Usando canais para comunicação segura

func main() {
    ch := make(chan int)
    result := 0

    // Goroutine produtora
    go func() {
        for i := 0; i < 100; i++ {
            ch <- i
        }
        close(ch)
    }()

    // Goroutine consumidora (única escritora)
    for n := range ch {
        result += n
    }
    fmt.Println(result)
}

Padrão "compartilhe por comunicação"

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2 // Cada worker tem seu próprio resultado
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Inicia workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Envia jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // Coleta resultados
    for a := 1; a <= 9; a++ {
        <-results
    }
}

6. Boas Práticas ao Usar o Detector

Integração com CI/CD

# Exemplo de pipeline GitHub Actions
test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/setup-go@v3
    - run: go test -race ./...  # Sempre execute testes com -race

Dicas importantes:

  • Execute testes com -race em todos os ambientes de desenvolvimento
  • Configure seu editor para executar go test -race automaticamente
  • Não ignore warnings do detector - mesmo que o programa "funcione"
  • Documente exceções quando desabilitar o detector

7. Casos Avançados e Armadilhas

Data races em testes com goroutines de fundo

func TestBackgroundWork(t *testing.T) {
    done := make(chan bool)
    result := 0

    go func() {
        time.Sleep(100 * time.Millisecond)
        result = 42 // Data race: goroutine de fundo vs teste principal
        done <- true
    }()

    // Teste termina antes da goroutine
    <-done
    if result != 42 {
        t.Error("resultado incorreto")
    }
}

Operações atômicas mal utilizadas

var counter int64

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
            // Ainda pode haver race se outra goroutine ler sem atomic
            if counter > 500 { // Leitura sem atomic - DATA RACE!
                fmt.Println("Acima de 500")
            }
        }()
    }
    wg.Wait()
}

8. Conclusão

O detector de race conditions do Go é uma ferramenta essencial para qualquer desenvolvedor que trabalhe com concorrência. Ele permite identificar e corrigir data races antes que causem problemas em produção. Lembre-se:

  1. Sempre use -race durante desenvolvimento e testes
  2. Interprete corretamente os relatórios de data race
  3. Prefira canais e comunicação segura a mutexes complexos
  4. Integre o detector em seu pipeline de CI/CD
  5. Entenda as limitações: o detector só encontra races que ocorrem durante a execução

Com essas práticas, você pode escrever código concorrente em Go com muito mais confiança e segurança.

Referências