Destino Dev

Blog

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

  1. Baixe e instale Go: https://go.dev/dl/
  2. Verifique: go version
  3. 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 run para rodar exemplos pequenos
  • go test -bench . -benchmem para benchmarks
  • go test -race para 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

  1. Prefira defer: Lock(); defer Unlock()
  2. Meça com -bench e -race; o -race é mais lento, mas detecta bugs críticos
  3. 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:

  1. Escritas fáceis e frequentes: Mutex.
  2. Muitas leituras curtas e poucas escritas: RWMutex.
  3. 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