---
title: "sync em Go: Mutex, WaitGroup, RWMutex e Once"
url: "https://golang.com.br/blog/sync-mutex-waitgroup-rwmutex-once-go/"
markdown_url: "https://golang.com.br/blog/sync-mutex-waitgroup-rwmutex-once-go.MD"
description: "sync.Mutex, RWMutex, WaitGroup, Once e atomic em Go: quando usar cada primitiva, como evitar deadlock e race condition, com exemplos práticos."
date: "2026-08-18"
author: "Golang Brasil"
---

# sync em Go: Mutex, WaitGroup, RWMutex e Once

sync.Mutex, RWMutex, WaitGroup, Once e atomic em Go: quando usar cada primitiva, como evitar deadlock e race condition, com exemplos práticos.


**Resposta rápida:** o pacote `sync` é a caixa de ferramentas de sincronização em memória do Go. Use **`Mutex`** para seções críticas curtas, **`RWMutex`** só quando leitura domina de verdade, **`WaitGroup`** para esperar um conjunto conhecido de goroutines, **`Once`** para inicialização lazy por processo e **`atomic`** para contadores/flags simples. Channels resolvem *comunicação*; `sync` resolve *estado compartilhado*. Em produção, meça com `-race`, evite lock ordering inconsistente e prefira designs que reduzam o estado compartilhado em vez de empilhar locks.

Este guia fecha o espaço entre o tutorial de [concorrência](/aprenda/concorrencia-go/), o guia de [channels](/blog/channels-go-comunicacao-goroutines-producao/) e o de [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/): o dia a dia de `Mutex`, `RWMutex`, `WaitGroup`, `Once` e `atomic` com exemplos copy-paste, armadilhas e checklist.

## Comparação rápida das primitivas

| Primativa | Problema que resolve | Evite quando… |
|---|---|---|
| `sync.Mutex` | Exclusão mútua de estado compartilhado | A seção crítica é longa ou faz I/O |
| `sync.RWMutex` | Muitos leitores, poucos escritores | O workload não é read-heavy mensurável |
| `sync.WaitGroup` | Esperar N tarefas terminarem | Você precisa cancelar/propagar o 1º erro → use errgroup |
| `sync.Once` | Inicializar um recurso uma vez por processo | Precisa de reset, ou coordenação entre pods |
| `sync/atomic` | Contadores, flags, swaps de ponteiro | Invariantes multi-campo |
| Channel | Passar ownership / eventos | Só para “proteger um int” |

## Mutex: a seção crítica bem-feita

`Mutex` garante que no máximo uma goroutine executa o bloco protegido. O padrão idiomático é **embedar o mutex junto do estado** que ele protege e nunca exportar o mutex solto.

```go
package cache

import "sync"

type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n++
}

func (c *Counter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.n
}
```

Regras práticas:

1. **Segure o lock o mínimo possível.** Não faça HTTP, SQL ou sleep com o mutex preso — isso transforma um contador em bottleneck global.
2. **Sempre `Unlock` via `defer`** na mesma função que fez `Lock`, salvo padrões avançados (e bem documentados) de unlock manual.
3. **Não copie structs que contêm Mutex.** Passe ponteiro. Copiar o mutex (e o estado) quebra a exclusão mútua.
4. **Documente o invariante.** Um comentário `// mu protege n e dirty` evita que o próximo commit acesse o campo “só rapidinho” sem lock.

Para um mapa concurrent-safe simples:

```go
type Store struct {
	mu sync.Mutex
	m  map[string]string
}

func NewStore() *Store {
	return &Store{m: make(map[string]string)}
}

func (s *Store) Set(k, v string) {
	s.mu.Lock()
	s.m[k] = v
	s.mu.Unlock()
}

func (s *Store) Get(k string) (string, bool) {
	s.mu.Lock()
	v, ok := s.m[k]
	s.mu.Unlock()
	return v, ok
}
```

Se o mapa for o *hot path* de leitura com poucas escritas, avalie `RWMutex` ou um snapshot imutável trocado via `atomic.Value` / `atomic.Pointer`.

## RWMutex: só quando a leitura realmente domina

`RLock` permite vários leitores; `Lock` é exclusivo. O custo do `RWMutex` é maior que o do `Mutex` simples — por isso a regra é **medir**, não “otimizar no feeling”.

```go
type ConfigView struct {
	mu   sync.RWMutex
	data map[string]string
}

func (c *ConfigView) Get(k string) (string, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	v, ok := c.data[k]
	return v, ok
}

func (c *ConfigView) Replace(next map[string]string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.data = next
}
```

Armadilhas clássicas:

- **Upgrade de lock:** segurar `RLock` e depois tentar `Lock` na mesma goroutine **deadlocka**. Solte o `RLock`, adquira `Lock`, ou redesenhe.
- **Escritor faminto:** leitores contínuos podem adiar o escritor. Se isso importa, prefira snapshot imutável.
- **Seção crítica “quase” read-only que muda um campo:** isso é escrita — use `Lock`.

Quando o padrão “trocar o mundo de uma vez” funciona melhor:

```go
package configsnap

import "sync/atomic"

type Snapshot struct {
	v atomic.Pointer[map[string]string]
}

func (s *Snapshot) Load() map[string]string {
	m := s.v.Load()
	if m == nil {
		return map[string]string{}
	}
	return *m
}

func (s *Snapshot) Store(m map[string]string) {
	// callers não devem mutar m depois do Store
	s.v.Store(&m)
}
```

Leituras ficam lock-free; escritas publicam um novo mapa. É o padrão de [configuração](/blog/configuracao-go-viper-variaveis-ambiente/) e feature flags em muitos serviços.

## WaitGroup: fan-out com contagem conhecida

`WaitGroup` espera um número conhecido de goroutines. É a ferramenta certa para “dispare N workers e junte o resultado”, desde que você **não** precise cancelar no primeiro erro — nesse caso, use [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/).

```go
package main

import (
	"fmt"
	"sync"
)

func main() {
	urls := []string{"a", "b", "c"}
	var wg sync.WaitGroup

	wg.Add(len(urls)) // Add ANTES do go
	for _, u := range urls {
		u := u
		go func() {
			defer wg.Done()
			fmt.Println("processou", u)
		}()
	}
	wg.Wait()
}
```

Checklist de WaitGroup:

1. `Add` **antes** de `go` (ou um `Add(n)` único com n conhecido).
2. Exatamente um `Done` por unidade adicionada — `defer wg.Done()` é o default seguro.
3. Não chame `Add` com delta negativo “criativo” para sinalizar; use `Done`.
4. Depois de `Wait`, não reutilize o mesmo WaitGroup sem garantir contagem zero. Em Go 1.25+ existe `WaitGroup.Go` (atalho para Add+goroutine+Done) — confira a doc da sua versão antes de adotar.

Para pipelines com cancelamento, prefira errgroup + `context` ([timeout e cancelamento](/blog/context-timeout-cancelamento-go/)).

## Once: inicialização lazy por processo

`Once` garante que uma função rode no máximo uma vez. Ideal para clientes HTTP compartilhados, parsers de regex caros, ou registro de métricas.

```go
package httpclient

import (
	"net/http"
	"sync"
	"time"
)

var (
	once   sync.Once
	client *http.Client
)

func HTTPClient() *http.Client {
	once.Do(func() {
		client = &http.Client{
			Timeout: 10 * time.Second,
			Transport: &http.Transport{
				MaxIdleConns:        100,
				MaxIdleConnsPerHost: 10,
			},
		}
	})
	return client
}
```

Limitações importantes:

- **Sem reset.** Se a inicialização falhar, Once ainda marca como executado. Capture o erro fora ou use um padrão `Once` + `atomic.Value` de resultado.
- **Por processo.** Em Kubernetes, cada pod inicializa o seu. Não é lock distribuído.
- **Não use para “rodar uma vez por request key”** — isso é [singleflight](/blog/singleflight-go-cache-stampede-deduplicacao/).

Padrão com erro:

```go
package lazydb

import (
	"context"
	"database/sql"
	"os"
	"sync"
)

type lazyDB struct {
	once sync.Once
	db   *sql.DB
	err  error
}

func (l *lazyDB) Conn(ctx context.Context) (*sql.DB, error) {
	l.once.Do(func() {
		l.db, l.err = sql.Open("pgx", os.Getenv("DATABASE_URL"))
		if l.err != nil {
			return
		}
		l.err = l.db.PingContext(ctx)
	})
	return l.db, l.err
}
```

Se `Ping` falhar na primeira chamada, todas as seguintes recebem o mesmo erro — pode ser o desejado (fail fast) ou não (aí você precisa de retry controlado fora do Once).

## atomic: contadores e flags sem Mutex

Para métricas locais, circuit breakers simples e flags, `sync/atomic` evita o custo do mutex:

```go
package stats

import "sync/atomic"

type Stats struct {
	hits atomic.Int64
}

func (s *Stats) Hit() { s.hits.Add(1) }

func (s *Stats) Hits() int64 { return s.hits.Load() }
```

Use `atomic.Bool`, `atomic.Int64`, `atomic.Pointer[T]` e `atomic.Value` da biblioteca padrão. Regras:

1. **Um campo, uma operação.** Não construa invariantes “a + b” com dois atomics separados sem barreira clara.
2. **Não misture atomic e Mutex no mesmo campo.**
3. Para mapas ou slices, prefira Mutex, concurrent map dedicado, ou snapshot via `atomic.Pointer`.

## Mutex vs channel: a decisão que evita overengineering

A frase clássica — *“não comunique compartilhando memória; compartilhe memória comunicando”* — é um norte, não um dogma.

**Prefira `sync` quando:**

- Várias goroutines leem/escrevem o mesmo cache, registry ou contador.
- A seção crítica é curta e local.
- Você está protegendo invariantes de uma struct.

**Prefira channels quando:**

- Uma goroutine “dona” do recurso recebe comandos (actor model leve).
- Você está montando pipeline, fan-in/fan-out, ou handoff de ownership.
- O dado *é* a mensagem (jobs, eventos), não um campo a proteger.

Exemplo de actor (channel) versus mutex no mesmo problema de saldo:

```go
package bank

// Actor: uma goroutine serializa as operações
type Bank struct {
	ops chan func(map[string]int)
}

func NewBank() *Bank {
	b := &Bank{ops: make(chan func(map[string]int))}
	go func() {
		balances := map[string]int{}
		for op := range b.ops {
			op(balances)
		}
	}()
	return b
}
```

Os dois estão corretos. O Mutex costuma ser mais simples para estado pequeno; o actor escala melhor quando a lógica ao redor do estado cresce (validação, journaling, métricas).

## Deadlocks e races: o que o `-race` não perdoa

Três bugs que aparecem em code review de produção:

### 1. Lock ordering

Goroutine A: `muA` → `muB`. Goroutine B: `muB` → `muA`. Deadlock clássico. **Ordene locks globalmente** (por endereço, por nome de recurso) ou agregue o estado sob um único mutex.

Veja também a página de erro [deadlock em goroutines](/erros/deadlock-goroutines/).

### 2. Add depois do go

```go
// ERRADO
go func() {
    wg.Add(1) // race com Wait
    defer wg.Done()
}()
wg.Wait()
```

### 3. Captura de variável de loop (pré-1.22) e dados sem sync

Mesmo com Go moderno, **escrever em um mapa/slice compartilhado sem sincronização** é data race. Rode sempre:

```bash
go test -race ./...
```

Em CI, trate race como falha de build. O guia de [race conditions](/erros/race-condition-data-race/) e o de [testes](/aprenda/testes-go/) cobrem o fluxo completo. Para observar contenção em produção, use [pprof](/blog/pprof-go-producao/) (mutex/block profiles) e [go tool trace](/blog/go-tool-trace-runtime-trace-latencia-goroutines/).

## Padrões de produção que funcionam

### Pool de workers com WaitGroup

Combine WaitGroup (junção) com channel bufferizado (fila) — o [worker pool](/blog/worker-pool-go-fila-jobs/) é o desenho completo. O esqueleto mínimo:

```go
jobs := make(chan Job, 64)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for j := range jobs {
			process(j)
		}
	}()
}
// envie jobs, feche o channel, depois:
close(jobs)
wg.Wait()
```

### Cache com singleflight + Mutex

Mutex protege o mapa local; singleflight evita stampede na origem. São complementary, não rivais.

### Graceful shutdown

No shutdown, pare de aceitar trabalho novo, feche channels de entrada e `Wait` nos workers — veja [graceful shutdown](/blog/graceful-shutdown-go-producao/). `WaitGroup` ou errgroup fecham o ciclo.

## Checklist antes de mergear código concorrente

1. Todo estado compartilhado tem dono claro (uma goroutine) **ou** lock/atomic documentado.
2. Nenhum I/O dentro de seção crítica sem justificativa escrita.
3. `go test -race` passa no pacote e nos vizinhos.
4. Lock ordering documentado se houver mais de um mutex.
5. WaitGroup: `Add` antes do `go`; `Done` com `defer`.
6. Once não esconde erro silencioso de init.
7. Contenção medida (benchmark ou mutex profile) se o path for quente — [benchmarks](/blog/benchmarks-go-testing-b-benchmem-pprof/).
8. Cancelamento: se o primeiro erro deve parar o resto, use errgroup + context, não só WaitGroup.
9. Em APIs HTTP, prefira [middleware](/blog/middleware-go-net-http-chi-gin-producao/) e timeouts a locks globais no handler.
10. Leitura adicional: [Effective Go 2026](/blog/effective-go-2026/) e o cheatsheet de [concorrência](/cheatsheet/concorrencia/).

## Quando NÃO usar sync

- **Estado por request:** passe valores; não grave em globais.
- **Distribuído:** Mutex local não coordena pods — use banco, Redis lock, ou fila ([SQS](/blog/sqs-go-filas-workers-aws/), [Redis Streams](/blog/redis-streams-go-filas-consumer-groups/)).
- **CPU pura e barata:** às vezes recalcular é mais barato que sincronizar ([sync.Pool](/blog/sync-pool-go-reutilizacao-objetos-performance/) e atomics existem para hot paths medidos).
- **API pública que “parece” thread-safe mas não documenta:** ou documente “not concurrent-safe”, ou torne seguro de verdade.

## Próximos passos

1. Audite um serviço seu: liste globais e structs compartilhadas; marque o mecanismo de sync de cada um.
2. Rode `go test -race ./...` e corrija o primeiro report — costuma ensinar mais que teoria.
3. Leia [channels em produção](/blog/channels-go-comunicacao-goroutines-producao/) e [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/) para fechar o triângulo comunicação / junção / cancelamento.
4. Se estiver montando API, combine com [context](/blog/context-timeout-cancelamento-go/), [rate limiting](/blog/rate-limiting-go-api-producao/) e [worker pools](/blog/worker-pool-go-fila-jobs/).
5. Para carreira e entrevistas, pratique explicar Mutex vs channel em voz alta — cai bastante em [perguntas de entrevista](/aprenda/perguntas-entrevista-go/) e no [plano de carreira](/carreira/plano-carreira-go-junior-senior-brasil/).

`sync` não é o inimigo da simplicidade: estado compartilhado mal definido é. Escolha a primitiva menor que expressa o invariante, meça contenção, e deixe channels / errgroup / filas cuidarem do resto do desenho concorrente.

## Perguntas frequentes

### Quando usar Mutex e quando usar channel em Go?

Use Mutex (ou atomic) quando várias goroutines compartilham o mesmo estado em memória e a seção crítica é curta. Prefira channels quando a comunicação é a mensagem em si — pipelines, fan-in, handoff de ownership. A regra prática: compartilhe memória sincronizando o acesso; não invente um channel só para proteger um contador. Detalhes no guia de [channels](/blog/channels-go-comunicacao-goroutines-producao/).

### Qual a diferença entre Mutex e RWMutex?

Mutex serializa todos os acessos. RWMutex permite múltiplos leitores simultâneos (`RLock`) e um único escritor (`Lock`). Só vale a pena se a leitura for claramente dominante e a seção crítica for mensurável; em hot paths curtos, Mutex simples costuma ser mais rápido e menos propenso a bugs de upgrade de lock.

### WaitGroup precisa de Add antes de cada go?

Sim. Chame `Add` (ou um `Add(n)` com n conhecido) **antes** de lançar a goroutine. Adicionar dentro da goroutine cria race com `Wait`. Garanta exatamente um `Done` por unidade — em geral com `defer wg.Done()`. Se você precisa cancelar no primeiro erro, troque para [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/).

### sync.Once garante execução única entre pods?

Não. `Once` é por processo. Cada réplica no Kubernetes tem o próprio Once. Para inicialização única no cluster, use um lock distribuído, um Job de bootstrap ou um recurso externo. Para deduplicar trabalho por chave dentro do processo, veja [singleflight](/blog/singleflight-go-cache-stampede-deduplicacao/).

### atomic substitui Mutex?

Para contadores, flags e ponteiros únicos, atomic costuma ser suficiente e mais barato. Para invariantes que envolvem vários campos, Mutex (ou um design imutável com snapshot) é mais seguro. Misturar atomic e Mutex no mesmo estado sem disciplina clara é fonte clássica de bugs sutis — e o detector de [race](/erros/race-condition-data-race/) não salva invariantes lógicos errados.
