O que é Mutex em Go?
Um mutex (mutual exclusion) é um mecanismo de sincronização do pacote sync que protege dados compartilhados contra acessos concorrentes. Quando múltiplas goroutines tentam ler e escrever na mesma variável simultaneamente, o resultado é uma race condition — um bug imprevisível que pode corromper dados, causar crashes ou produzir resultados inconsistentes. O mutex resolve isso garantindo que apenas uma goroutine por vez acesse a região crítica do código.
Na prática, pense no mutex como uma trava de banheiro: quando uma goroutine chama Lock(), ela entra e tranca a porta. Qualquer outra goroutine que tente Lock() fica esperando na fila até a primeira chamar Unlock(). Esse mecanismo simples é a base de toda sincronização baseada em memória compartilhada em Go.
Embora Go incentive o uso de channels para comunicação entre goroutines com o famoso lema “não comunique compartilhando memória, compartilhe memória comunicando”, existem cenários onde mutex é a solução mais simples e performática — especialmente para proteger estado interno de structs e caches em memória.
sync.Mutex — o mutex básico
O sync.Mutex oferece duas operações: Lock() e Unlock():
package main
import (
"fmt"
"sync"
)
type ContadorSeguro struct {
mu sync.Mutex
valor int
}
func (c *ContadorSeguro) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock()
c.valor++
}
func (c *ContadorSeguro) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
func main() {
contador := &ContadorSeguro{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contador.Incrementar()
}()
}
wg.Wait()
fmt.Println(contador.Valor()) // Sempre 1000
}
Sem o mutex, o resultado seria imprevisível — algumas operações valor++ seriam perdidas porque ler e escrever não são operações atômicas. O mutex garante que cada incremento acontece completamente antes do próximo começar.
O padrão defer Unlock
Sempre use defer para Unlock():
func (c *ContadorSeguro) Operacao() error {
c.mu.Lock()
defer c.mu.Unlock() // garante unlock mesmo com panic ou return antecipado
if c.valor < 0 {
return fmt.Errorf("valor negativo: %d", c.valor)
}
c.valor *= 2
return nil
}
O defer garante que o unlock acontece mesmo se a func retorna antecipadamente por um erro ou se ocorre um panic. Sem defer, um return esquecido ou uma exceção podem deixar o mutex travado para sempre — causando deadlock.
sync.RWMutex — leitores e escritores
O sync.RWMutex é uma variante otimizada para cenários com muitas leituras e poucas escritas:
type Cache struct {
mu sync.RWMutex
dados map[string]string
}
func NovaCache() *Cache {
return &Cache{
dados: make(map[string]string),
}
}
// Leitura — múltiplas goroutines podem ler simultaneamente
func (c *Cache) Get(chave string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
valor, ok := c.dados[chave]
return valor, ok
}
// Escrita — acesso exclusivo, bloqueia todas as leituras
func (c *Cache) Set(chave, valor string) {
c.mu.Lock()
defer c.mu.Unlock()
c.dados[chave] = valor
}
// Exclusão com escrita exclusiva
func (c *Cache) Delete(chave string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.dados, chave)
}
A diferença é crucial para performance:
| Operação | sync.Mutex | sync.RWMutex |
|---|---|---|
| Leitura simultânea | Bloqueada | Permitida |
| Escrita simultânea | Bloqueada | Bloqueada |
| Leitura durante escrita | Bloqueada | Bloqueada |
Se 95% das operações são leituras (cenário típico de caches), RWMutex permite que todas as leituras aconteçam em paralelo, desbloqueando apenas para escritas. Em APIs REST com alta taxa de leitura, isso faz uma diferença significativa de performance.
Quando usar RWMutex vs Mutex
// Use sync.Mutex quando:
// - Leituras e escritas são balanceadas (50/50)
// - Seções críticas são muito curtas
// - Simplicidade é mais importante que performance
// Use sync.RWMutex quando:
// - Leituras são muito mais frequentes que escritas (90%+)
// - Múltiplos leitores simultâneos melhoram throughput
// - Seções de leitura são relativamente lentas
Race conditions — o problema que mutex resolve
Uma race condition acontece quando o resultado do programa depende da ordem de execução de goroutines:
// BUG: race condition — NÃO faça isso
var saldo int = 100
func sacar(valor int) {
if saldo >= valor {
// Outra goroutine pode sacar entre a verificação e a subtração
time.Sleep(time.Millisecond) // simula processamento
saldo -= valor
fmt.Printf("Sacou %d, saldo: %d\n", valor, saldo)
}
}
// Duas goroutines sacando ao mesmo tempo
go sacar(80)
go sacar(80)
// Possível resultado: saldo = -60 (ambas passam na verificação)
Detectando race conditions
Go tem um detector de race conditions embutido:
# Compilar e executar com detector de race
go run -race main.go
# Executar testes com detector
go test -race ./...
# Build com detector (para staging/QA)
go build -race -o app
O flag -race adiciona instrumentação que detecta acessos concorrentes inseguros em runtime. Use-o em todos os testes e ambientes de QA — o overhead de performance (~5-10x) é aceitável para detecção de bugs.
Deadlock — quando mutex trava tudo
Um deadlock acontece quando goroutines ficam esperando umas pelas outras infinitamente:
// DEADLOCK: goroutine A espera B, B espera A
var mu1, mu2 sync.Mutex
// Goroutine A
go func() {
mu1.Lock()
defer mu1.Unlock()
time.Sleep(time.Millisecond)
mu2.Lock() // espera mu2 (que B segurou)
defer mu2.Unlock()
}()
// Goroutine B
go func() {
mu2.Lock()
defer mu2.Unlock()
time.Sleep(time.Millisecond)
mu1.Lock() // espera mu1 (que A segurou)
defer mu1.Unlock()
}()
Como evitar deadlocks
- Ordene os locks — sempre adquira mutexes na mesma ordem global
- Minimize a seção crítica — segure o lock pelo menor tempo possível
- Evite chamar funções externas com lock mantido — podem tentar adquirir outro lock
- Use timeout com context quando possível
// CORRETO: ordem consistente de locks
func transferir(contaA, contaB *Conta, valor int) {
// Ordenar por ID para garantir ordem consistente
primeiro, segundo := contaA, contaB
if contaA.ID > contaB.ID {
primeiro, segundo = contaB, contaA
}
primeiro.mu.Lock()
defer primeiro.mu.Unlock()
segundo.mu.Lock()
defer segundo.mu.Unlock()
contaA.Saldo -= valor
contaB.Saldo += valor
}
sync.Once — execução única garantida
sync.Once garante que uma func seja executada exatamente uma vez, independente de quantas goroutines a chamem:
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
fmt.Println("Inicializando conexão (acontece uma vez)")
instance = &Database{
Pool: conectar("postgres://localhost/app"),
}
})
return instance
}
É o padrão singleton thread-safe mais idiomático em Go. Diferente de usar mutex manualmente, sync.Once é otimizado internamente — após a primeira execução, chamadas subsequentes não têm overhead de sincronização.
sync.WaitGroup — esperando goroutines
sync.WaitGroup coordena a espera por múltiplas goroutines:
func processarLote(urls []string) []Resultado {
var (
wg sync.WaitGroup
mu sync.Mutex
resultados []Resultado
)
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resultado := buscar(u)
mu.Lock()
resultados = append(resultados, resultado)
mu.Unlock()
}(url)
}
wg.Wait() // espera todas terminarem
return resultados
}
Note o padrão: WaitGroup coordena o ciclo de vida das goroutines, enquanto o mutex protege o slice compartilhado. Em código mais idiomático, você usaria channels em vez do mutex para coletar resultados.
sync.Map — map thread-safe
Para cenários específicos, Go oferece sync.Map como alternativa a map + mutex:
var cache sync.Map
// Armazenar
cache.Store("chave", "valor")
// Buscar
valor, ok := cache.Load("chave")
// Buscar ou armazenar atomicamente
valor, loaded := cache.LoadOrStore("chave", "valor-padrao")
// Deletar
cache.Delete("chave")
// Iterar
cache.Range(func(chave, valor any) bool {
fmt.Printf("%v: %v\n", chave, valor)
return true // continue iterando
})
sync.Map é otimizado para dois cenários: quando chaves são escritas uma vez e lidas muitas vezes, ou quando goroutines diferentes acessam conjuntos disjuntos de chaves. Fora desses cenários, map + RWMutex geralmente tem melhor performance.
Mutex vs Channel — quando usar cada um
| Cenário | Mutex | Channel |
|---|---|---|
| Proteger estado interno de struct | Melhor | Excessivo |
| Cache em memória | Melhor | Inadequado |
| Pipeline de dados entre goroutines | Inadequado | Melhor |
| Fan-out/fan-in | Complicado | Melhor |
| Contador simples | Melhor | Excessivo |
| Orquestração de goroutines | Complicado | Melhor |
Regra prática: use mutex para proteger dados, use channels para coordenar goroutines. Para padrões avançados de concorrência, explore os padrões de concorrência em Go.
Boas práticas com mutex
- Sempre use defer Unlock — previne locks esquecidos em caminhos de erro
- Minimize a seção crítica — não faça I/O ou cálculos pesados com lock mantido
- Coloque o mutex perto dos dados — campo do struct, não variável global
- Use RWMutex quando leituras dominam (cache, configuração)
- Nunca copie mutex — passe ponteiros para structs com mutex
- Execute testes com
-race— detecta problemas antes de produção
Para aprender mais sobre concorrência em Go, explore o glossário de goroutines, channels e os tutoriais de concorrência.
Perguntas frequentes (FAQ)
Qual a diferença entre sync.Mutex e sync.RWMutex?
sync.Mutex permite apenas um acesso por vez (exclusivo), enquanto sync.RWMutex permite múltiplas leituras simultâneas mas exclusividade para escritas. Use RWMutex quando leituras são muito mais frequentes que escritas — em caches de APIs REST, por exemplo, isso pode melhorar o throughput significativamente.
Como detectar race conditions em Go?
Use o flag -race nos comandos go run, go test e go build. O detector de race conditions instrumenta o código e reporta acessos concorrentes inseguros em runtime. Sempre execute testes com -race em CI/CD — bugs de concorrência são os mais difíceis de reproduzir e debugar.
Mutex pode causar deadlock?
Sim. Deadlock acontece quando duas goroutines tentam adquirir os mesmos mutexes em ordem diferente, ou quando uma goroutine tenta Lock() um mutex que já possui (Go não tem mutex reentrant). Para evitar: ordene locks consistentemente, minimize seções críticas e nunca chame funções externas com lock mantido.
Quando usar sync.Map em vez de map com mutex?
Use sync.Map em dois cenários: quando chaves são escritas uma vez e lidas repetidamente, ou quando goroutines diferentes acessam conjuntos disjuntos de chaves. Para a maioria dos outros cenários — especialmente com iterações frequentes ou slices como valores — um map protegido por RWMutex tem melhor performance e é mais tipo-seguro.