Mutex vs RWMutex em Go: quando usar, analogias e exemplos passo a passo
Por John Pires · 16 de março de 2026

Mutex vs RWMutex em Go: quando usar, analogias e exemplos passo a passo
Por que aprender isso em 2026
Concorrência permanece central em aplicações modernas: servidores HTTP, CLIs paralelos, pipelines de dados e sistemas embarcados exigem controle seguro de acesso a recursos compartilhados. Em Go, sync.Mutex e sync.RWMutex são ferramentas primitivas de sincronização que todo desenvolvedor precisa dominar. Em 2026, com mais código sendo executado em multicore e em ambientes de latência crítica, escolhas incorretas de locking são causa comum de contenção e latência.
Este artigo concentra-se em explicar o que cada primitiva faz, quando cada uma é adequada e como migrar de uma para outra sem quebrar a lógica. A intenção é prática: analogias, pequenos testes e um mini-projeto que evidencia diferenças de performance e segurança.
O problema real
Concorrência tem duas dimensões: segurança (não corromper dados) e performance (não bloquear demais). Mutex resolve segurança mas pode penalizar performance. RWMutex traz leitura concorrente, mas tem trade-offs: potencial starvation e maior complexidade. Saber as regras básicas reduz bugs e melhora latência.
Analogia curta
Pense em uma biblioteca: um Mutex é a porta trancada para todos — apenas uma pessoa entra de cada vez; um RWMutex permite que várias pessoas leiam livros ao mesmo tempo, mas quando alguém quer escrever no livro, fecha-se a sala para todos. Essa analogia ajuda a raciocinar sobre quando permitir leituras simultâneas e quando exigir exclusividade.
Configuração do ambiente
Você precisa do Go instalado. Recomendado Go 1.21+ em 2026. Testes serão feitos com a ferramenta padrão, go test e go run. Se estiver em Linux ou macOS, instale via instalador oficial ou gerenciador de pacotes; Windows com instalador MSI. Uma IDE ou editor com suporte a go vet e go test (VSCode + Go extension) facilita a vida.
Também vamos usar a flag -race para detectar condições de corrida e o pacote testing/benchmark para medir latência. Ter CPU multicore (2+ cores) ajuda a observar diferenças entre Mutex e RWMutex em leituras concorrentes.
Instalação rápida
- Baixe e instale Go: https://go.dev/dl/
- Verifique:
go version - Crie um módulo:
go mod init exemplo/mutexdemo
Esses passos criam ambiente mínimo para compilar e rodar os exemplos fornecidos neste artigo.
Ferramentas úteis
go runpara rodar exemplos pequenosgo test -bench . -benchmempara benchmarksgo test -racepara detectar data races- Perf: pprof, trace do runtime para cargas reais
Dica: habilite módulos do Go e mantenha dependências mínimas. Tudo aqui usa apenas stdlib.
Hello World progressivo
Aqui vamos construir três versões de um contador compartilhado: sem proteção (mostra race), com Mutex e com RWMutex. Primeiro exemplo: contador sem sincronização para ilustrar race.
Exemplo 1: sem proteção (mostra race)
O código abaixo incrementa uma variável a partir de múltiplas goroutines. Execute com go run e com go run -race para sentir o problema.
package main
import (
"fmt"
"sync"
)
func main() {
var n int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
n++ // data race
}()
}
wg.Wait()
fmt.Println("n=", n)
}
O que faz: cria 1000 goroutines incrementando a variável n sem sincronização.
O que acontece ao rodar: sem -race o resultado é indeterminado; com -race o runtime detecta e relata data race. Esse é o ponto de partida para justificar mutexes.
Exemplo 2: proteção com sync.Mutex
A versão com Mutex envolve cada incremento em Lock/Unlock, garantindo exclusividade. É simples e seguro, mas pode ser lento se houver muitas leituras que não modificam.
package main
import (
"fmt"
"sync"
)
func main() {
var n int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
n++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("n=", n)
}
O que faz: protege n com mu, tornando as operações atômicas em relação a outras goroutines.
O que acontece: operação correta e determinística. Em cargas intensas de escrita, Mutex é apropriado e simples.
Exemplo 3: sync.RWMutex para leituras concorrentes
RWMutex permite múltiplos leitores simultâneos, mas escritores exigem exclusividade. Exemplo abaixo simula muitas leituras e poucas escritas.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var data int
var mu sync.RWMutex
var wg sync.WaitGroup
// leitores
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
mu.RLock()
_ = data // leitura simulada
time.Sleep(5 * time.Millisecond)
mu.RUnlock()
}(i)
}
// escritor
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
data = 42
mu.Unlock()
}()
wg.Wait()
fmt.Println("done, data=", data)
}
O que faz: leitores usam RLock/RUnlock e o escritor usa Lock/Unlock. Isso reduz contenção quando leituras são frequentes.
O que acontece: múltiplos leitores podem executar em paralelo; o escritor aguarda até que leitores terminem e bloqueia novas leituras até completar a escrita.
Mini-projeto prático
Vamos construir uma API HTTP simples que expõe uma estrutura em memória: um mapa de chaves para contadores. A API permite ler contadores (GET) e incrementá-los (POST). O objetivo é medir e comparar Mutex vs RWMutex em cenários típicos de leitura intensiva.
Estrutura do projeto
- server.go: servidor HTTP
- storage.go: implementação com Mutex ou RWMutex
- bench_test.go: benchmarks com go test -bench
Abaixo um snippet funcional da versão com RWMutex; substitua por Mutex e compare o benchmark.
package main
import (
"encoding/json"
"net/http"
"sync"
)
// storage.go (versão rwmutex)
type Store struct {
mu sync.RWMutex
m map[string]int
}
func NewStore() *Store {
return &Store{m: make(map[string]int)}
}
func (s *Store) Get(key string) int {
s.mu.RLock()
v := s.m[key]
s.mu.RUnlock()
return v
}
func (s *Store) Inc(key string) int {
s.mu.Lock()
v := s.m[key] + 1
s.m[key] = v
s.mu.Unlock()
return v
}
func main() {
store := NewStore()
http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
k := r.URL.Query().Get("k")
v := store.Get(k)
json.NewEncoder(w).Encode(map[string]int{"value": v})
})
http.HandleFunc("/inc", func(w http.ResponseWriter, r *http.Request) {
k := r.URL.Query().Get("k")
v := store.Inc(k)
json.NewEncoder(w).Encode(map[string]int{"value": v})
})
http.ListenAndServe(":8080", nil)
}
O que faz: server simples com duas rotas. Get retorna valor atual, Inc incrementa. Storage usa RWMutex para reduzir contenção em reads.
Como testar: rode o servidor e dispare centenas de requisições GET em paralelo (ab) e poucas POST, então repita com Mutex. Use benchmarks ou ferramentas de carga para comparar latência e throughput.
Benchmarks e interpretação
Crie bench_test.go que dispara N leitores e M escritores em goroutines e meça tempo com testing.B. Compare usando mu sync.Mutex em vez de sync.RWMutex. Em geral:
- Se leituras >> escritas, RWMutex tende a ganhar
- Se escritas frequentes, Mutex simples costuma ser mais estável
- RWMutex tem overhead em cada RLock/RUnlock; em leitura muito leve pode perder para Mutex
Dica: sempre medir no seu cenário real; microbenchmarks podem enganar se não refletirem proporções de leitura/escrita.
Erros comuns, armadilhas e dicas práticas
Listo problemas e práticas que vejo em times iniciantes ao usar Mutex/RWMutex em Go.
Erros comuns
- Esquecer Unlock/RUnlock: leva a deadlocks. Use defer imediatamente após Lock/RLock:
mu.Lock(); defer mu.Unlock(). - Usar RWMutex com seção crítica longuíssima: leitores podem bloquear escritores por muito tempo.
- Confundir Mutex e RWMutex: RWMutex não protege de lógica de upgrade (RLock->Lock) sem liberar RLock primeiro, o que pode levar a deadlock se houver outros leitores.
Exemplo de armadilha: tentar promover RLock para Lock sem liberar. Nunca faça:
// anti-pattern: não promova RLock->Lock sem liberar
mu.RLock()
// ... decide que precisa escrever
mu.Lock() // deadlock se outros leitores existirem
// ...
mu.Unlock()
mu.RUnlock()
Correção: libere RLock antes de solicitar Lock. Às vezes é necessário revalidar estado após reaquisição do Lock.
Dicas práticas
- Prefira defer: Lock(); defer Unlock()
- Meça com -bench e -race; o -race é mais lento, mas detecta bugs críticos
- Considere alternativas sem lock: atomic, channels ou sharding de locks para reduzir contenção
Shard example: em vez de um mapa global protegido por um mutex, mantenha N mapas e N mutexes; selecione shard por hash da chave. Isso reduz contenção sem complexidade de RWMutex.
Recursos para continuar estudando
Listo links e livros para aprofundar concorrência em Go e padrões relacionados.
- Documentação oficial: https://pkg.go.dev/sync
- Go Concurrency Patterns (talks do Rob Pike)
- Livro: "Concurrency in Go" (Katherine Cox-Buday) — bom para padrões de alto nível
- Artigos sobre sharding, atomic.Value e pprof/runtime/trace para diagnóstico
Além disso, pratique com exercícios: implemente caches concorrentes, leaky bucket rate limiter e job queues com worker pools; compare uso de Mutex, RWMutex, channels e atomic para cada caso.
Conclusão prática
Resumo objetivo: use Mutex quando a simplicidade e escrita frequente prevalecem; use RWMutex quando há muitas leituras e poucas escritas, mas apenas após medir. Tenha cuidado com promoção de locks e com seções críticas longas.
Regras rápidas para decidir:
- Escritas fáceis e frequentes: Mutex.
- Muitas leituras curtas e poucas escritas: RWMutex.
- Se contenção persiste: tente sharding, atomic ou redesign para evitar compartilhamento.
Próximo passo prático: clone o mini-projeto, rode benchmarks com -bench e -race, troque RWMutex por Mutex e compare throughput e latência em um ambiente com múltiplos núcleos. Isso solidifica intuição e fornece dados concretos para decisões arquiteturais.
Este artigo ofereceu analogias, exemplos funcionais e armadilhas comuns. Aplique os padrões em código real e meça. Decisões de sincronização são sempre um balanço entre segurança e performance: medir é obrigatório.
Assuntos: Go, concurrency, mutex, rwmutex
Leia também
Rate limiting distribuído em Go: token bucket local com fallback em Redis
Combinar token bucket local com fallback em Redis em Go reduz latência e mantém controle global. Este artigo explica padrões, scripts Lua, exemplos em Go e…
Retries exponenciais com jitter em Go: backoff, Context e cancelamento
Aprenda a implementar retries exponenciais com jitter em Go, integrando backoff, context.Context e cancelamento para evitar vazamentos e reduzir carga em…
API REST em Go: worker pool com backpressure, Context e desligamento gracioso
Implementar um worker pool em Go para uma API REST exige backpressure, Context para cancelamento e shutdown gracioso. Veja anti-patterns, versões corrigidas e…
Cache LRU concorrente em Go: singleflight, sharding e benchmarks práticos
Implementação prática de cache LRU concorrente em Go: comparo mutex global, sharding e singleflight, mostro código funcional, benchmarks e armadilhas para…
Diagnóstico e redução de tail-latency em Go: pprof, contenção e GOMAXPROCS
Como identificar e reduzir tail-latency em Go: pprof, contenção (locks, sync.Pool) e GOMAXPROCS. Mostro o erro comum (mutex global) e correções práticas com…
Downloader concorrente em Go: goroutines, channels, cancelamento e progresso
Aprenda a construir um downloader concorrente em Go usando goroutines, channels e context para cancelamento e progresso. Exemplo funcional, CLI simples e…