Race condition em Go: o que é e como o detector de corrida ajuda
Race condition em Go acontece quando a ordem entre goroutines muda o resultado. Veja como ler o relatório do -race e corrigir com mutex ou canal.
Publicada em · 23 min de leitura
Uma , ou condição de corrida, acontece quando o resultado de um programa depende da ordem em que eventos concorrentes acontecem, e pelo menos uma dessas ordens produz um resultado errado. Em Go, o caso mais comum é o de duas ou mais goroutines lendo e escrevendo a mesma variável sem combinar nada entre si.
Esta aula ensina o que é a corrida, por que ela escapa dos testes comuns, a diferença entre race condition e , o que já vem com a toolchain (go run -race e go test -race), duas formas de corrigir o problema, com e com , e por que o detector só enxerga o que executa. Ao final, você deve conseguir olhar um trecho concorrente e apontar quais acessos estão sem coordenação, em vez de confiar que o teste passou.
Pré-requisito: Goroutine, para saber o que é lançar trabalho concorrente. Os exemplos usam waitGroup para esperar as goroutines terminarem. Esta aula não aprofunda o modelo de memória. Esse assunto e as primitivas de ficam na aula de sync.
Mapa da aula
| Parte | Seções | O que você vai aprender |
|---|---|---|
| 1. Fundamentos e intuição | 1 a 2 | O que é uma race condition e por que a ordem dos eventos deixou de ser previsível |
| 2. Race condition e data race | 3 a 4 | A diferença entre os dois termos e um contador que perde incrementos |
| 3. O detector de corrida | 5 a 6 | go run -race e go test -race, o que o relatório diz e quanto ele custa |
| 4. Corrigir com mutex e canal | 7 a 8 | A trava que protege o acesso e a goroutine |
| 5. Os limites do detector | 9 a 10 | Race condition sem data race e por que o silêncio não prova nada |
| 6. Prática e consolidação | 11 a 15 | Exercícios com gabarito, glossário, o que não foi confirmado, fontes e relacionados |
Parte 1: Fundamentos e intuição
1. O que é uma race condition
Pense no último pão de queijo da bandeja numa festa. Duas pessoas estendem a mão ao mesmo tempo. Quem chega primeiro leva, e a outra fica com a mão no ar olhando para a bandeja vazia. Ninguém errou, a bandeja não quebrou e a contagem dos pães não ficou torta. O resultado apenas dependeu de quem chegou antes. Se a cena se repetir dez vezes, a vencedora pode mudar a cada vez. Isso é uma race condition: um resultado que depende da ordem ou do tempo relativo de eventos que não foram coordenados.
O ponto que importa é a coordenação. Se o resultado correto não depende da ordem, não há corrida. Se algum resultado possível está errado e nada impede que ele aconteça, há corrida. Em código sequencial, uma linha só começa depois que a anterior terminou, então a ordem vem de graça. Com duas goroutines, a ordem entre as linhas de uma e de outra não é garantida. Cabe a você decidir quais ordens são aceitáveis e criar a regra que as impõe.
Um termo precisa de definição curta já aqui: data race é um caso mais técnico de corrida, em que duas goroutines acessam a mesma posição de memória ao mesmo tempo, sem sincronização entre os acessos, e pelo menos um deles é uma escrita. A versão completa, com a diferença entre os dois termos, fica na seção 3.
2. Por que a ordem dos eventos deixou de ser previsível
Antes de haver várias linhas de execução, a ordem das coisas era a ordem do código. A concorrência surgiu para aproveitar os núcleos da CPU e para não deixar o programa parado esperando disco ou rede. A partir daí, quem decide qual goroutine roda e quando é o runtime do Go, por meio do Scheduler. Em uma máquina com vários núcleos, duas goroutines podem executar no mesmo instante, o que é paralelismo. Nenhuma dessas decisões aparece no código, e ambas mudam de uma execução para outra conforme a carga, o número de núcleos e o momento em que cada goroutine é colocada para rodar.
Isso explica por que a corrida engana. O código parece correto, o teste local passa, e o erro só aparece quando a calha de acontecer. Quem nunca viu um bug desses provavelmente ainda não rodou o código com carga de verdade.
Pense em ouro++. A linha parece um passo só, mas a máquina costuma executá-la em três etapas: ler o valor atual, somar 1 e escrever o resultado de volta. Se duas goroutines executam essas etapas ao mesmo tempo, elas podem se intercalar de formas que a linha escondida no código não mostra. A linguagem não faz ouro++ ser uma operação indivisível. Para isso existem os mutexes e o pacote sync/atomic, tratados nas seções 7 e 8 e na aula de sync. O problema existe em outras linguagens também: as Thread do sistema operacional enfrentam o mesmo desafio. O Go oferece primitivas de coordenação e uma ferramenta de diagnóstico. Sozinha, nenhuma das duas elimina o problema.
Parte 2: Race condition e data race
3. Race condition e data race: dois termos, duas ideias
Os dois termos aparecem misturados em tutoriais e em revisões de código. Vale separá-los, porque a ferramenta que o Go oferece enxerga apenas um deles.
A documentação oficial do detector define a data race assim, em tradução livre: ela ocorre quando duas goroutines acessam a mesma variável ao mesmo tempo e pelo menos um desses acessos é uma escrita. O que falta nessa definição, e que vem da teoria de concorrência, é a ausência de sincronização entre os dois acessos. Sem ela, nenhuma ordem entre eles está garantida. O modelo de memória do Go só garante ordem entre acessos ligados por uma sincronização. Entre os demais, não há garantia alguma, e o detalhe fica para a aula de sync.
A race condition é uma situação de lógica. Ela descreve qualquer resultado que dependa da ordem e que possa estar errado. A regra de negócio decide se um resultado está errado. Por isso os dois conceitos não coincidem. Uma data race pode ou não produzir um resultado visivelmente errado, e uma race condition pode existir sem nenhuma data race. O exemplo da reserva, na seção 9, mostra esse segundo caso: cada acesso à memória está protegido e a regra de negócio, mesmo assim, quebra.
A distinção importa porque o detector de corrida procura data races. Ele não conhece a sua regra de negócio. Um bug de reserva pode existir com zero alertas, e um teste verde com o detector ligado só diz que aquele caminho não cometeu data race.
4. Um contador que perde incrementos
O programa abaixo cria mil goroutines, e cada uma soma 1 mil vezes na mesma variável. O resultado deveria ser um milhão, mas nada garante esse valor, e o programa não emite erro algum.
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
const goroutines = 1000
const incrementos = 1000
var ouro int
var wg sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < incrementos; j++ {
ouro++
}
}()
}
wg.Wait()
esperado := goroutines * incrementos
fmt.Printf("esperado: %d\n", esperado)
fmt.Printf("obtido: %d\n", ouro)
fmt.Printf("perdidos: %d (GOMAXPROCS=%d)\n", esperado-ouro, runtime.GOMAXPROCS(0))
}
O diagrama abaixo mostra uma das intercalações possíveis. O valor começa em 10 só para facilitar a leitura, porque o programa real começa em zero. A faixa amarela marca o trecho em que as duas goroutines leem antes de qualquer escrita. Essa é a janela da corrida. Repare que a escrita da goroutine B apaga o +1 que a goroutine A já tinha feito. Nada no código sinaliza isso, e a perda some sem erro.
Rodei o programa três vezes sem o detector, em um MacBook Pro com Apple M1 Max e dez núcleos lógicos (GOMAXPROCS=10). Os resultados foram estes:
esperado: 1000000
obtido: 533058
perdidos: 466942 (GOMAXPROCS=10)
esperado: 1000000
obtido: 586416
perdidos: 413584 (GOMAXPROCS=10)
esperado: 1000000
obtido: 1000000
perdidos: 0 (GOMAXPROCS=10)
A terceira execução acertou, e isso não prova nada: a corrida é intermitente. Em oito execuções sem o detector, as perdas variaram de 0 a 885 736. Este exemplo usa mil goroutines fazendo mil somas cada uma para tornar o problema visível. Com menos concorrência, o erro pode ficar escondido por muito tempo, e um teste curto não vai pegá-lo.
Parte 3: O detector de corrida
5. O detector de corrida: go run -race e go test -race
O Go traz o detector de corrida no próprio compilador. A flag -race instrumenta cada leitura e cada escrita de memória do programa e registra qual goroutine tocou em cada endereço e se havia uma ordem de sincronização entre os acessos. Sincronização, aqui, é qualquer mecanismo que estabelece uma ordem entre dois acessos, como um mutex ou um canal. Quando dois acessos concorrentes não têm essa ordem e pelo menos um deles é escrita, o detector emite um aviso. A ferramenta é derivada do ThreadSanitizer, uma biblioteca de análise dinâmica do Google, segundo o post oficial que apresentou o detector em 2013.
Os comandos mais usados são estes:
go run -race ./contador-bug
go test -race ./...
go build -race -o app-diagnostico .
O custo precisa ser conhecido antes de qualquer decisão. A documentação oficial estima, para um programa típico, de 5 a 10 vezes mais memória e de 2 a 20 vezes mais tempo de execução. Por isso o binário com -race serve para testes e para a integração contínua, e não para a produção. Num pipeline, rodar go test -race ./... é um bom padrão para os pacotes que têm goroutines.
Quando o detector encontra uma corrida, o programa termina com código de saída 66 por padrão. O go run mostra isso como exit status 66. A flag funciona em darwin/arm64, segundo go help build na toolchain que usei nesta revisão.
6. Lendo o relatório: uma execução com race e uma sem
Esta é a saída real do go run -race no contador sem proteção. Os caminhos foram encurtados para contador-bug/main.go, e o detector emitiu dois blocos; mostro o primeiro:
==================
WARNING: DATA RACE
Read at 0x00c000012138 by goroutine 53:
main.main.func1()
contador-bug/main.go:21 +0x88
Previous write at 0x00c000012138 by goroutine 131:
main.main.func1()
contador-bug/main.go:21 +0x98
Goroutine 53 (running) created at:
main.main()
contador-bug/main.go:18 +0x70
Goroutine 131 (finished) created at:
main.main()
contador-bug/main.go:18 +0x70
==================
...
esperado: 1000000
obtido: 695532
perdidos: 304468 (GOMAXPROCS=10)
Found 2 data race(s)
exit status 66
Leia o relatório de cima para baixo. A primeira linha diz o tipo do problema. Em seguida vêm a leitura (Read at) e a escrita anterior (Previous write at), cada uma com a goroutine que a fez e a linha do código. O bloco created at mostra onde cada goroutine foi criada, que é o ponto de partida para descobrir quem disparou o trabalho. Os dois endereços são idênticos, o que confirma que se trata da mesma variável. A linha 21 do arquivo é ouro++, e a linha 18 é o go func().
Repare que o programa imprimiu o resultado errado depois do aviso. O detector relata a corrida e deixa o programa seguir.
Esta é a saída do mesmo contador depois da correção com mutex, rodada com -race:
esperado: 1000000
obtido: 1000000
perdidos: 0 (GOMAXPROCS=10)
O exit status foi 0 e nenhum aviso apareceu. Um relatório limpo significa que o detector não encontrou corrida nos acessos que rodaram. A seção 10 explica por que essa frase tem limite.
Parte 4: Corrigir com mutex e canal
7. Corrigir com mutex
O mutex é a chave do banheiro de um posto de gasolina. Só entra quem pegou a chave, e quem esquece de devolvê-la deixa a fila inteira cruzando as pernas. Na prática, Lock bloqueia a goroutine até que outra chame Unlock. O trecho entre os dois é chamado de , porque só uma goroutine por vez pode executá-lo.
A versão corrigida muda apenas a declaração das variáveis e o corpo do laço. O restante do programa é igual ao anterior:
var (
mu sync.Mutex
ouro int
wg sync.WaitGroup
)
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < incrementos; j++ {
mu.Lock()
ouro++
mu.Unlock()
}
}()
}
A trava protege a variável apenas se todo acesso passa por ela. Uma única função que esquece o Lock derruba a proteção de todas as outras, como o caso de SomarRapido na seção 10. Dentro de funções, o padrão mais seguro é mu.Lock() seguido de defer mu.Unlock() na linha de baixo, para que um retorno antecipado não deixe a trava presa. Esquecer o Unlock é um erro caro, e quem nunca deixou uma trava presa ainda não teve um deadlock às três da manhã. A documentação do pacote sync também avisa que um Mutex não deve ser copiado depois do primeiro uso, o que pode ser verificado com go doc sync.Mutex na toolchain instalada.
A trava tem custo. Goroutines que disputam o mesmo mutex esperam umas pelas outras. Para um contador isolado a espera é pequena, mas em regiões críticas longas ela aparece no tempo de resposta.
8. Corrigir com canal: uma goroutine dona do estado
Imagine um restaurante com uma única pessoa na chapa. Os garçons não encostam na chapa. Eles anotam o pedido num papel e entregam a ela, que é a única a mexer na comida. Num programa, a mesma ideia aparece quando uma goroutine é dona de uma variável e as outras só enviam pedidos para ela por um canal:
pedidos := make(chan int)
resultado := make(chan int)
go func() {
ouro := 0
for soma := range pedidos {
ouro += soma
}
resultado <- ouro
}()
var wg sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < incrementos; j++ {
pedidos <- 1
}
}()
}
wg.Wait()
close(pedidos)
ouro := <-resultado
O trecho acima é o centro do programa completo, que foi rodado com e sem -race. Nele, o canal pedidos é sem buffer. Isso quer dizer que quem envia espera até a dona receber, e o próprio canal controla o ritmo. A goroutine dona é a única que toca em ouro, por isso a variável não precisa de trava. Os produtores só conhecem o canal.
A ordem das operações de fechamento importa. O close(pedidos) só pode vir depois de wg.Wait(), porque enviar para um canal já fechado causa pânico. A especificação da linguagem diz, em tradução livre, que enviar ou fechar um canal fechado causa um pânico em tempo de execução. Todo mundo esquece o close uma vez: sem ele, o range da dona nunca termina e <-resultado espera para sempre.
A saída com -race foi esta, idêntica à do mutex:
esperado: 1000000
obtido: 1000000
perdidos: 0 (GOMAXPROCS=10)
No diagrama, cada produtor envia um pedido e o canal segura um pedido por vez. A caixa da dona carrega o selo “Única”, que resume a regra: só ela escreve. A seta laranja, de dona para ouro, é o único caminho de escrita, e por isso a caixa do ouro não aparece com trava. Os pacotes azuis são os pedidos dos produtores.
A escolha entre mutex e canal depende da forma do problema. Use mutex quando o estado é simples e a região protegida é curta, como um contador ou um mapa de cache. Use canal quando existe um fluxo de trabalho, com produtores, consumidores e um dono do estado que serializa as mudanças. Um canal usado só para imitar uma trava costuma ficar mais complicado que o mutex que ele substitui. Para um contador isolado, sync/atomic também resolve, mas esse caminho fica para a aula de sync.
Parte 5: Os limites do detector
9. Race condition sem data race: o caso da reserva
Este programa não tem nenhuma data race. Todo acesso ao mapa passa pelo mutex mu. Mesmo assim, Ana e Bruno reservam a mesma poltrona:
package main
import (
"fmt"
"sync"
"time"
)
// Assentos guarda quem ocupa cada poltrona. Todo acesso ao mapa passa pelo mutex,
// então o detector de corrida não tem o que reclamar.
type Assentos struct {
mu sync.Mutex
ocupados map[string]string
}
func (a *Assentos) Livre(assento string) bool {
a.mu.Lock()
defer a.mu.Unlock()
_, ocupado := a.ocupados[assento]
return !ocupado
}
func (a *Assentos) Reservar(assento, passageiro string) {
a.mu.Lock()
defer a.mu.Unlock()
a.ocupados[assento] = passageiro
}
func main() {
a := &Assentos{ocupados: map[string]string{}}
var wg sync.WaitGroup
for _, p := range []string{"Ana", "Bruno"} {
wg.Add(1)
go func(passageiro string) {
defer wg.Done()
// Checar e reservar são duas travas separadas. Entre elas, a outra goroutine pode agir.
if a.Livre("12A") {
time.Sleep(10 * time.Millisecond) // só alarga a janela para o efeito aparecer sempre
a.Reservar("12A", passageiro)
fmt.Printf("%s reservou 12A\n", passageiro)
} else {
fmt.Printf("%s viu 12A ocupado\n", passageiro)
}
}(p)
}
wg.Wait()
fmt.Printf("ocupante final de 12A: %s\n", a.ocupados["12A"])
}
A saída com -race, em uma das execuções, foi esta:
Bruno reservou 12A
Ana reservou 12A
ocupante final de 12A: Ana
Não houve aviso, e o exit status foi 0. Em 20 execuções seguidas, as duas goroutines reservaram a poltrona em todas. A ordem das linhas e o ocupante final variaram. O erro está entre Livre e Reservar: são duas travas separadas, e entre elas a outra goroutine pode agir. A espera de 10 milissegundos existe só para tornar o efeito estável nesta demonstração.
O diagrama mostra a mesma situação na ordem do tempo. A faixa vermelha marca a janela entre a checagem e a reserva, em que as duas respostas são “livre”. Cada acesso ao mapa passou por uma trava, então nenhum deles é uma data race. A regra de negócio, porém, foi quebrada.
A correção é colocar a checagem e a escrita dentro da mesma trava, para que nenhuma outra goroutine entre no meio:
// ReservarSeLivre checa e escreve dentro da mesma trava: nenhuma outra goroutine entra no meio.
func (a *Assentos) ReservarSeLivre(assento, passageiro string) bool {
a.mu.Lock()
defer a.mu.Unlock()
if _, ocupado := a.ocupados[assento]; ocupado {
return false
}
time.Sleep(10 * time.Millisecond) // a mesma janela de antes, agora fechada pela trava
a.ocupados[assento] = passageiro
return true
}
Com essa versão, em 20 execuções houve sempre uma reserva e um “viu ocupado”. A saída típica com -race foi:
Bruno viu 12A ocupado
Ana reservou 12A
ocupante final de 12A: Ana
O detector respondeu à pergunta que sabe responder: dois acessos à mesma memória se intercalaram sem ordem? A pergunta sobre a regra de negócio, se a poltrona pode ter dois donos, ficou com quem escreve o código.
10. Ausência de alerta não prova ausência de corrida
Este é um erro comum com o detector. Considere um pacote com dois caminhos de escrita: Somar, que usa mutex, e SomarRapido, que esqueceu a trava. Ninguém escreveu teste para o segundo caminho.
var (
mu sync.Mutex
total int
)
// Somar é o caminho normal: protegido pelo mutex.
func Somar(n int) {
mu.Lock()
total += n
mu.Unlock()
}
// SomarRapido é um caminho secundário que esqueceu o mutex.
func SomarRapido(n int) {
total += n
}
O teste abaixo exercita apenas Somar:
func TestSomar(t *testing.T) {
total = 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Somar(1)
}()
}
wg.Wait()
if total != 10 {
t.Fatalf("total = %d, queria 10", total)
}
}
Com go test -race -v, a saída foi:
=== RUN TestSomar
--- PASS: TestSomar (0.00s)
PASS
ok race-aula/caminho-nao-testado 1.184s
Até aqui tudo parece certo. Depois de adicionar um segundo teste que chama SomarRapido em paralelo, com a mesma estrutura, go test -race -v passa a relatar a corrida e falha:
==================
WARNING: DATA RACE
Read at 0x000102c9c270 by goroutine 14:
race-aula/caminho-nao-testado.SomarRapido()
.../caminho-nao-testado/main.go:22 +0x6c
...
testing.go:1867: race detected during execution of test
--- FAIL: TestSomarRapido (0.00s)
FAIL
FAIL race-aula/caminho-nao-testado 0.195s
O exit status do go test foi 1. Sem -race, o mesmo go test -v passa para os dois testes, porque a corrida existe mas não derruba o resultado daquela execução. O mesmo código recebeu três respostas diferentes: verde sem o detector, verde com o detector enquanto o caminho inseguro não rodava, e vermelho com o detector quando ele rodou.
A documentação do detector diz, em tradução livre, que ele só encontra corridas que acontecem durante a execução e, por isso, não consegue encontrar corridas em caminhos de código que não foram executados. A mesma página sugere rodar um binário compilado com -race sob uma carga realista para encontrar mais corridas. Isso aponta para duas coisas: cobertura de testes e carga parecida com a de produção. Um teste verde com o detector ligado é uma informação sobre o que rodou, e nada além disso.
O diagrama resume os dois caminhos de silêncio. O silêncio correto acontece quando os acessos concorrentes estão ordenados por trava ou canal. O silêncio enganoso acontece quando a corrida existe no código, mas nenhum dos acessos que a provocam rodou. Nos dois casos o relatório fica vazio, e só o primeiro significa que os acessos estavam certos.
Pergunta de entrevistaO race detector garante que o código não tem corrida?
Não. O detector, ativado com -race, marca acessos concorrentes à mesma memória, com pelo menos uma escrita, sem sincronização. Ele só enxerga os caminhos que a execução realmente percorreu. Por isso uso -race nos testes e com carga, mas não considero a ausência de alertas como prova. Ele também não pega race condition sem data race, como uma reserva que verifica e depois grava sem atomicidade.
Parte 6: Prática e consolidação
11. Exercícios práticos graduados
Tente resolver cada exercício antes de abrir o gabarito. Todos os gabaritos foram rodados nesta revisão.
Exercício 1: achar as corridas com o detector
Objetivo: o programa abaixo tem um painel de visitas que é atualizado por 400 goroutines. Rode com go run -race e identifique as linhas que aparecem no relatório. Depois corrija o código sem mudar a assinatura de Registrar.
type Painel struct {
visitas int
ultimo string
}
func (p *Painel) Registrar(caminho string) {
p.visitas++
p.ultimo = caminho
}
O detector reportou três corridas nesta versão, com Found 3 data race(s) e exit status 66. As linhas envolvidas são a de p.visitas++ e a de p.ultimo = caminho.
Solução de referência:
type Painel struct {
mu sync.Mutex
visitas int
ultimo string
}
func (p *Painel) Registrar(caminho string) {
p.mu.Lock()
defer p.mu.Unlock()
p.visitas++
p.ultimo = caminho
}
Com a correção, a saída com -race foi visitas: 400, ultimo: /checkout e exit status 0. O valor de ultimo depende da ordem das goroutines, então pode ser qualquer um dos quatro caminhos. Aqui o que conta é a ausência de corrida, e o valor final fica em segundo plano.
Exercício 2: duas compras na última unidade
Objetivo: dois compradores tentam levar a última unidade de um estoque. Cada chamada a Disponivel e a Vender usa o mutex, e ainda assim o estoque pode ficar negativo. Explique por que o detector não reclama, e corrija.
type Estoque struct {
mu sync.Mutex
unidades int
}
func (e *Estoque) Disponivel() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.unidades > 0
}
func (e *Estoque) Vender() {
e.mu.Lock()
defer e.mu.Unlock()
e.unidades--
}
No main, e := &Estoque{unidades: 1} e duas goroutines, Carla e Diego, executam if e.Disponivel() { time.Sleep(10 * time.Millisecond); e.Vender() }.
Em três execuções sem -race, as duas compras aconteceram nas três, e o estoque terminou em -1. Com -race, o detector não emitiu nenhum aviso. Cada acesso ao campo unidades estava protegido, e a falha está entre a checagem e a venda.
Solução de referência:
// Comprar checa e vende dentro da mesma trava.
func (e *Estoque) Comprar() bool {
e.mu.Lock()
defer e.mu.Unlock()
if e.unidades <= 0 {
return false
}
time.Sleep(10 * time.Millisecond) // só alarga a janela, como no exemplo da aula
e.unidades--
return true
}
Com essa versão, a saída com -race foi:
Carla comprou a última unidade
Diego encontrou esgotado
unidades restantes: 0
Exercício 3: fazer o teste expor o caminho esquecido
Objetivo: a partir do código de Somar e SomarRapido da seção 10, escreva um teste que faça o go test -race acusar a corrida em SomarRapido, sem alterar o código de produção.
Solução de referência:
func TestSomarRapido(t *testing.T) {
total = 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
SomarRapido(1)
}()
}
wg.Wait()
}
Com go test -race, esse teste falha com race detected during execution of test e exit status 1. Sem -race, ele passa. A correção do próprio SomarRapido é a mesma da seção 7: adicionar mu.Lock() e mu.Unlock() ao redor de total += n.
12. Glossário técnico
- Race condition: situação em que o resultado de um programa depende da ordem ou do tempo relativo de eventos concorrentes, e alguma dessas ordens produz um resultado errado segundo a regra de negócio.
- Data race: acesso concorrente à mesma posição de memória, sem sincronização entre os acessos, com pelo menos uma escrita.
- Janela de corrida: o intervalo de tempo em que outra goroutine pode agir entre dois passos que deveriam ser indivisíveis.
- Intercalação: uma das ordens possíveis em que as operações de duas ou mais goroutines podem se alternar.
- Atualização perdida: efeito de uma escrita que sobrescreve outra feita a partir de um valor antigo, apagando a alteração anterior.
- Seção crítica: trecho de código que só pode ser executado por uma goroutine por vez, protegido por uma trava.
- Mutex: trava de exclusão mútua.
Lockbloqueia até que outra goroutine chameUnlock. - Canal: mecanismo de comunicação entre goroutines. Sem buffer, quem envia espera quem recebe.
- Dona do estado: goroutine que é a única a ler e escrever uma variável, enquanto as outras enviam pedidos para ela.
- Sincronização: mecanismo que estabelece uma ordem entre dois acessos, como um mutex ou um canal.
- Detector de corrida: ferramenta ativada pela flag
-race, derivada do ThreadSanitizer, que observa os acessos de memória durante a execução e relata data races que acontecem nela. - Happens-before: relação de ordem que a sincronização estabelece entre dois acessos. Se um acesso acontece antes de outro por essa relação, o segundo enxerga o que o primeiro escreveu.
13. O que não foi confirmado
As saídas e os números vêm de um MacBook Pro com Apple M1 Max, dez núcleos lógicos, macOS (Darwin 27.0.0) e Go 1.27.2 em darwin/arm64. Linux e outras versões do Go não foram executados nesta revisão, e o comportamento do detector nessas plataformas não foi conferido.
As perdas do contador variam entre execuções. A faixa observada, de 0 a 885 736 em oito execuções, não é uma estatística. Não medi a probabilidade de corrida para esse programa, e nenhuma frequência é afirmada aqui.
A espera de 10 milissegundos nas demos de reserva e de estoque serve para tornar o efeito estável. O comportamento desses programas sem a espera não foi medido.
O custo do detector, de 5 a 10 vezes mais memória e de 2 a 20 vezes mais tempo, vem da documentação oficial. Não medi esse custo no programa desta aula.
A frase sobre o modelo de memória do Go, de que só há ordem entre acessos ligados por uma sincronização, é uma leitura da documentação do detector e da prática. O texto completo do modelo de memória (go.dev/ref/mem) foi citado pela documentação, mas não foi lido nesta revisão.
A referência ao livro de Donovan e Kernighan aparece nas fontes apenas como indicação de leitura. O capítulo sobre variáveis compartilhadas não foi conferido nesta revisão.
14. Fontes e leituras recomendadas
Consultadas em 3 de novembro de 2026.
- The Go Authors. “Data Race Detector”. Documentação oficial. https://go.dev/doc/articles/race_detector. A página não mostra data de publicação ou de atualização.
- Vyukov, Dmitry; Gerrand, Andrew. “Introducing the Go Race Detector”. The Go Blog, 26 de junho de 2013. https://go.dev/blog/race-detector.
- The Go Authors. Especificação da linguagem, seção sobre canais, consultada na cópia local da toolchain go1.27.2 (
doc/go_spec.html). - The Go Authors.
go help buildego doc sync.Mutex, consultados na toolchain local go1.27.2 (darwin/arm64). - The Go Authors. “The Go Memory Model”. https://go.dev/ref/mem. Citada pela documentação do detector e não lida nesta revisão.
- Donovan, Alan A. A.; Kernighan, Brian W. The Go Programming Language. Addison-Wesley, 2015. Capítulo sobre variáveis compartilhadas.
15. Relacionados
- Go
- Goroutine
- waitGroup
- Channels
- Select
- Context
- sync
- deadlock
- Thread
- Scheduler
- Trilha de Go