Condições de corrida (data races) são um dos bugs mais comuns e difíceis de diagnosticar em programas concorrentes. Em Go, o modelo de concorrência baseado em goroutines e canais reduz a complexidade, mas ainda é possível introduzir races quando múltiplas goroutines acessam a mesma variável sem sincronização. Felizmente, Go oferece um detector de corridas embutido que pode ser ativado durante testes e execução.

Nesta aula, vamos explorar o que é uma data race, como usar a flag -race para detectá-la, e as principais técnicas de sincronização para evitá-la, incluindo o uso de mutexes, canais e boas práticas de memória compartilhada.

O que é data race

Uma data race ocorre quando duas ou mais goroutines acessam a mesma variável simultaneamente, e pelo menos um dos acessos é de escrita. O resultado é imprevisível: o valor lido pode estar desatualizado ou corrompido, levando a comportamentos erráticos e bugs difíceis de reproduzir.

Por exemplo, considere um contador compartilhado incrementado por várias goroutines sem sincronização. Cada incremento envolve leitura, incremento e escrita; se duas goroutines executarem isso ao mesmo tempo, uma pode sobrescrever o valor da outra.

package main

import (
	"fmt"
	"sync"
)

var counter int

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			counter++
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(counter)
}

O resultado esperado é 1000, mas devido à data race, o valor pode ser menor ou variar a cada execução.

go test -race

Para detectar data races, Go fornece o race detector, ativado com a flag -race em comandos como go run, go build e go test. Ele instrumenta o código para monitorar todos os acessos a memória e reportar qualquer race detectado em tempo de execução.

Para testar o programa anterior, salve-o como race.go e execute:

go run -race race.go

O detector imprimirá um aviso detalhado com a localização do race, incluindo as goroutines envolvidas e as linhas de código. Em testes, use go test -race ./... para verificar todo o pacote.

Exemplo de saída:

==================
WARNING: DATA RACE
Read at 0x00c0000b4010 by goroutine 7:
  main.main.func1()
      .../race.go:14 +0x3e

Previous write at 0x00c0000b4010 by goroutine 6:
  main.main.func1()
      .../race.go:14 +0x54

Goroutine 7 (running) created at:
  main.main()
      .../race.go:12 +0x8a

Goroutine 6 (finished) created at:
  main.main()
      .../race.go:12 +0x8a
==================

O detector é uma ferramenta essencial, mas tem custo de performance (cerca de 5-10x mais lento) e maior uso de memória, por isso só deve ser usado durante desenvolvimento e testes.

Sincronização

A forma mais direta de evitar data races é sincronizar o acesso a dados compartilhados. Go oferece primitivas como sync.Mutex, sync.RWMutex e canais. O uso de mutexes garante que apenas uma goroutine por vez execute uma seção crítica.

Exemplo corrigido com mutex:

package main

import (
	"fmt"
	"sync"
)

var (
	counter int
	mu      sync.Mutex
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			mu.Lock()
			counter++
			mu.Unlock()
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(counter)
}

Outra abordagem é usar canais para comunicação, seguindo o lema: "Não se comunique compartilhando memória; compartilhe memória se comunicando." Por exemplo, uma goroutine pode ser a única proprietária de um dado, e outras enviam requisições por canais.

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	go func() {
		for i := 0; i < 1000; i++ {
			ch <- 1
		}
		close(ch)
	}()
	sum := 0
	for v := range ch {
		sum += v
	}
	fmt.Println(sum)
}

Nesse exemplo, não há variável compartilhada; a goroutine principal soma valores recebidos do canal.

Memória compartilhada

Em Go, a memória compartilhada entre goroutines exige cuidado. Sempre que múltiplas goroutines leem e escrevem na mesma variável, é necessário sincronizar. Uma boa prática é limitar o escopo dos dados compartilhados e usar tipos seguros como sync.Map ou estruturas com mutex embutido.

Evite compartilhar ponteiros entre goroutines sem sincronização. Se precisar compartilhar, considere usar um canal para transferir a posse. Por exemplo, um pool de workers pode receber tarefas por um canal e enviar resultados por outro, sem compartilhar estado.

package main

import (
	"fmt"
	"sync"
)

type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) Inc() {
	c.mu.Lock()
	c.n++
	c.mu.Unlock()
}

func (c *Counter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.n
}

func main() {
	var wg sync.WaitGroup
	c := &Counter{}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			c.Inc()
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(c.Value())
}

Outra dica: use go vet para detectar possíveis races em tempo de compilação (embora limitado). E lembre-se: o race detector só encontra races que ocorrem durante a execução; testes exaustivos com concorrência são recomendados.

Referências

Exercícios

  1. Crie um programa que lance 10 goroutines, cada uma incrementando um contador compartilhado 100 vezes sem sincronização. Execute com go run -race e observe o aviso. Corrija o programa usando um mutex.
  2. ✓ Resposta:
    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var counter int
    var mu sync.Mutex
    
    func main() {
    	var wg sync.WaitGroup
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go func() {
    			for j := 0; j < 100; j++ {
    				mu.Lock()
    				counter++
    				mu.Unlock()
    			}
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    	fmt.Println(counter)
    }
  3. Modifique o programa do exercício 1 para usar um canal com buffer de tamanho 1 como mutex. Dica: envie um token para o canal antes de acessar o contador e retire após.
  4. ✓ Resposta:
    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var counter int
    var sem = make(chan struct{}, 1)
    
    func main() {
    	var wg sync.WaitGroup
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go func() {
    			for j := 0; j < 100; j++ {
    				sem <- struct{}{}
    				counter++
    				<-sem
    			}
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    	fmt.Println(counter)
    }
  5. Escreva um teste para uma função que retorna o valor de um contador após incrementos concorrentes. Use go test -race para verificar se há data race. A função deve usar sincronização.
  6. ✓ Resposta:
    package counter
    
    import (
    	"sync"
    	"testing"
    )
    
    type SafeCounter struct {
    	mu sync.Mutex
    	n  int
    }
    
    func (c *SafeCounter) Inc() {
    	c.mu.Lock()
    	c.n++
    	c.mu.Unlock()
    }
    
    func (c *SafeCounter) Value() int {
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	return c.n
    }
    
    func TestConcurrentIncrement(t *testing.T) {
    	c := &SafeCounter{}
    	var wg sync.WaitGroup
    	for i := 0; i < 100; i++ {
    		wg.Add(1)
    		go func() {
    			c.Inc()
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    	if c.Value() != 100 {
    		t.Errorf("esperado 100, obtido %d", c.Value())
    	}
    }
  7. Explique por que o seguinte código pode ter uma data race e corrija-o usando canais: duas goroutines escrevem em um mapa compartilhado sem sincronização.
  8. ✓ Resposta:O código tem race porque duas goroutines escrevem no mapa sem sincronização. Correção com canal: uma goroutine é a única proprietária do mapa, outras enviam operações por um canal.
    package main
    
    import "fmt"
    
    type op struct {
    	key   string
    	value int
    	resp  chan struct{}
    }
    
    func main() {
    	ch := make(chan op)
    	m := make(map[string]int)
    	go func() {
    		for o := range ch {
    			m[o.key] = o.value
    			o.resp <- struct{}{}
    		}
    	}()
    
    	for i := 0; i < 10; i++ {
    		go func(i int) {
    			resp := make(chan struct{})
    			ch <- op{key: fmt.Sprintf("key%d", i), value: i, resp: resp}
    			<-resp
    		}(i)
    	}
    }
  9. Crie um programa que simule um banco com múltiplas contas e transferências concorrentes. Use um mutex por conta para evitar races. Execute com -race para garantir que está correto.
  10. ✓ Resposta:
    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    type Account struct {
    	mu sync.Mutex
    	balance int
    }
    
    func (a *Account) Deposit(amount int) {
    	a.mu.Lock()
    	a.balance += amount
    	a.mu.Unlock()
    }
    
    func (a *Account) Withdraw(amount int) bool {
    	a.mu.Lock()
    	defer a.mu.Unlock()
    	if a.balance >= amount {
    		a.balance -= amount
    		return true
    	}
    	return false
    }
    
    func Transfer(from, to *Account, amount int) bool {
    	if from.Withdraw(amount) {
    		to.Deposit(amount)
    		return true
    	}
    	return false
    }
    
    func main() {
    	alice := &Account{balance: 1000}
    	bob := &Account{balance: 500}
    	var wg sync.WaitGroup
    	for i := 0; i < 100; i++ {
    		wg.Add(1)
    		go func() {
    			Transfer(alice, bob, 10)
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    	fmt.Println("Alice:", alice.balance, "Bob:", bob.balance)
    }