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çãosync.Mutexsync.RWMutex
Leitura simultâneaBloqueadaPermitida
Escrita simultâneaBloqueadaBloqueada
Leitura durante escritaBloqueadaBloqueada

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

  1. Ordene os locks — sempre adquira mutexes na mesma ordem global
  2. Minimize a seção crítica — segure o lock pelo menor tempo possível
  3. Evite chamar funções externas com lock mantido — podem tentar adquirir outro lock
  4. 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árioMutexChannel
Proteger estado interno de structMelhorExcessivo
Cache em memóriaMelhorInadequado
Pipeline de dados entre goroutinesInadequadoMelhor
Fan-out/fan-inComplicadoMelhor
Contador simplesMelhorExcessivo
Orquestração de goroutinesComplicadoMelhor

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

  1. Sempre use defer Unlock — previne locks esquecidos em caminhos de erro
  2. Minimize a seção crítica — não faça I/O ou cálculos pesados com lock mantido
  3. Coloque o mutex perto dos dados — campo do struct, não variável global
  4. Use RWMutex quando leituras dominam (cache, configuração)
  5. Nunca copie mutex — passe ponteiros para structs com mutex
  6. 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.