Go: Primitivas de sincronização do Go
Os canais são usados para a comunicação entre goroutines, e o pacote
syncé utilizado para proteger o estado compartilhado — o Go oferece um conjunto completo de ferramentas para programação concorrente.
Quando várias goroutines precisam ler ou gravar na mesma variável, são necessárias primitivas de sincronização para garantir a segurança. Nesta lição, você aprenderá todas as ferramentas essenciais e estratégias de seleção do pacote sync do Go.
1. Você aprenderá
sync.Mutexmutexsync.RWMutexmutex de leitura e gravação (as leituras são não exclusivas)sync.Once: Executar uma vezsync.Map: Um mapa seguro contra concorrênciasync/atomicoperações atômicas- detector de corrida (sinalizador
-race) sync.Poolpool de objetos temporários- Estratégias para a seleção de primitivas de sincronização
2. Uma história real de um engenheiro especializado em alta concorrência
(1) Problema: 100 goroutines gravam no contador simultaneamente, fazendo com que os dados fiquem completamente corrompidos
Charlie é engenheiro de backend de uma plataforma de comércio eletrônico e precisa contar o número de pedidos por segundo:
“100 goroutines estavam processando pedidos simultaneamente, e cada uma delas incrementava o contador em 1 após concluir um pedido. No primeiro dia após o lançamento, os dados estavam completamente errados — o painel mostrava 1.000 pedidos por segundo, mas havia apenas 300 pedidos no banco de dados. Meu chefe me perguntou: ‘Para onde foi o dinheiro desses 700 pedidos?’”
Ele abriu o código:
// Bad code: concurrent write to shared variable, no lock
var counter int
func processOrder(orderID string) {
// ... process order
counter++ // Non-atomic operation! Equivalent to:
// temp = counter
// temp = temp + 1
// counter = temp <- 3 goroutines executing this step simultaneously = chaos
}
counter++ não é uma operação atômica — ela se divide em três etapas: leitura, incremento em um e gravação. Se duas goroutines lerem simultaneamente counter=10, incrementarem em um e, em seguida, gravarem de volta, o resultado será counter=11 em vez de 12 — esse é um exemplo clássico de disputa de dados.
(2) Solução em Go: Mutex
// counter.go
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
c.value++ // Protected by the lock; only one goroutine can execute at a time
c.mu.Unlock()
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var counter SafeCounter
var wg sync.WaitGroup
// 100 goroutines incrementing simultaneously
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("Final value: %d (expected 100)\n", counter.Value())
}
Resultado:
Final value: 100 (expected 100)
(3) Retornos: Bloqueados x Desbloqueados
| Situação | Resultado | Conflito de dados |
|---|---|---|
| Concorrência sem bloqueios | 95/100/97 (aleatório) | ✅ Sim (condição de corrida detectada) |
| Proteção por mutex | 100 | ❌ Nenhuma |
| operação atômica | 100 | ❌ Nenhuma |
go run -race main.go pode detectar conflitos de acesso. Se o acesso simultâneo a variáveis compartilhadas não for protegido por bloqueios, a opção -race emitirá um aviso em tempo de execução. Recomenda-se habilitar sempre -race no CI/CD.
3. sync.Mutex Mutex
(1) Noções básicas sobre mutex
package main
import (
"fmt"
"sync"
)
type Account struct {
mu sync.Mutex
balance float64
}
func (a *Account) Deposit(amount float64) {
a.mu.Lock()
a.balance += amount
a.mu.Unlock()
}
func (a *Account) Withdraw(amount float64) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance < amount {
return fmt.Errorf("insufficient balance: have %.2f, need %.2f", a.balance, amount)
}
a.balance -= amount
return nil
}
func (a *Account) Balance() float64 {
a.mu.Lock()
defer a.mu.Unlock()
return a.balance
}
func main() {
acc := Account{balance: 1000}
var wg sync.WaitGroup
// 10 concurrent deposits
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
acc.Deposit(100)
}()
}
wg.Wait()
fmt.Printf("Balance: %.2f\n", acc.Balance())
}
(2) ▶ Exemplo: Adiar o padrão de desbloqueio (recomendado)
// Recommended: Lock then immediately defer Unlock
func (a *Account) SafeMethod() {
a.mu.Lock()
defer a.mu.Unlock()
// All operations are within the lock
a.balance += 100
a.balance -= 50
// Even if there is a panic in between, defer ensures Unlock
}
// Not recommended: manual Unlock
func (a *Account) UnsafeMethod() {
a.mu.Lock()
a.balance += 100
a.mu.Unlock() // If there is a return or panic in between, Unlock won't be called → deadlock
}
Lock, é preciso chamar Unlock. Sempre use defer a.mu.Unlock() em vez de chamar manualmente Unlock no final — se houver vários caminhos de retorno, esquecer de chamar Unlock em qualquer um deles resultará em um impasse.
4. sync.RWMutex – Bloqueio de leitura e gravação
(1) Princípios dos bloqueios de leitura e gravação
package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}
// Read operation: RLock (concurrent reads, non-exclusive)
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
// Write operation: Lock (exclusive, blocks all reads and writes)
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
cache := NewCache()
var wg sync.WaitGroup
// Multiple concurrent reads (RLock is non-exclusive)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 3; j++ {
if v, ok := cache.Get("key"); ok {
fmt.Printf("Reader %d: %s\n", id, v)
}
time.Sleep(10 * time.Millisecond)
}
}(i)
}
// One writer
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(5 * time.Millisecond)
cache.Set("key", "value")
fmt.Println("Writer: write complete")
}()
wg.Wait()
}
(2) ▶ Exemplo: Comparação de desempenho entre Mutex e RWMutex
package main
import (
"sync"
"testing"
)
type DataStore struct {
mu sync.RWMutex
value int
}
func BenchmarkMutexRead(b *testing.B) {
store := DataStore{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
store.mu.Lock()
_ = store.value
store.mu.Unlock()
}
})
}
func BenchmarkRWMutexRead(b *testing.B) {
store := DataStore{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
store.mu.RLock()
_ = store.value
store.mu.RUnlock()
}
})
}
// go test -bench=. -benchmem
(3) Mutex x RWMutex
| Recurso | Mutex | RWMutex |
|---|---|---|
| Leitura e Leitura | Exclusão mútua | Não exclusão mútua |
| Leitura e gravação | Exclusão mútua | Exclusão mútua |
| Gravação e gravação | Exclusão mútua | Exclusão mútua |
| Casos de uso | Proporção leitura-gravação 1:1 | As leituras superam em muito as gravações (por exemplo, cache) |
| Desempenho de leitura | Lento (serializado) | Rápido (leituras simultâneas) |
5. sync.Once: Executar uma vez
(1) Noções básicas
package main
import (
"fmt"
"sync"
)
var (
config map[string]string
configOnce sync.Once
)
func loadConfig() {
configOnce.Do(func() {
fmt.Println("Initializing config...")
config = map[string]string{
"host": "localhost",
"port": "8080",
}
})
}
func main() {
var wg sync.WaitGroup
// 100 goroutines call simultaneously, but init executes only once
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
loadConfig()
}()
}
wg.Wait()
fmt.Printf("config: %v\n", config)
}
Resultado:
Initializing config...
config: map[host:localhost port:8080]
(2) sync.Once x sinalizadores manuais
| Método | Seguro para múltiplas threads | Tamanho do código |
|---|---|---|
if !initialized { ... } |
❌ Conflito de acesso aos dados | Pequeno |
sync.Once |
✅ Garantias do Go | Mínimo |
função init() |
✅ Executada quando o pacote é carregado | Mínima |
sync.Once garante que a função dentro de Do seja executada apenas uma vez, mesmo que seja chamada simultaneamente por 1.000 goroutines. Sua implementação interna utiliza operações atômicas e mutexes, o que a torna muito eficiente.
6. sync.Map: Um mapa seguro para concorrência
(1) Noções básicas sobre o sync.Map
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// Concurrent writes
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m.Store(fmt.Sprintf("key-%d", n), n*100)
}(i)
}
wg.Wait()
// Read
m.Range(func(key, value interface{}) bool {
fmt.Printf("%s = %d\n", key, value)
return true
})
// LoadOrStore: returns if exists, stores if not
actual, loaded := m.LoadOrStore("key-0", 999)
fmt.Printf("LoadOrStore: actual=%d, loaded=%v\n", actual, loaded)
}
(2) ▶ Exemplo: sync.Map x map+Mutex
package main
import (
"sync"
"testing"
)
// map + Mutex
type MutexMap struct {
mu sync.Mutex
items map[string]int
}
func (m *MutexMap) Store(key string, value int) {
m.mu.Lock()
m.items[key] = value
m.mu.Unlock()
}
func (m *MutexMap) Load(key string) (int, bool) {
m.mu.Lock()
v, ok := m.items[key]
m.mu.Unlock()
return v, ok
}
func BenchmarkMutexMap(b *testing.B) {
m := &MutexMap{items: make(map[string]int)}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Store("key", 1)
m.Load("key")
}
})
}
func BenchmarkSyncMap(b *testing.B) {
var m sync.Map
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Store("key", 1)
m.Load("key")
}
})
}
// go test -bench=. -benchmem
(3) Casos de uso do sync.Map
| Cenário | Recomendação | Motivo |
|---|---|---|
| Chave gravada uma vez | sync.Map | Otimização de leitura (poucas gravações, muitas leituras) |
| Atualizações frequentes de chaves | map+Mutex | as gravações no sync.Map são um pouco mais lentas |
| Conjunto de chaves em expansão | sync.Map | Evite bloquear o mapa inteiro |
| Cenário simples | mapa + Mutex | Mais intuitivo |
| Requisitos de alto desempenho | Teste de desempenho específico | Determinado por testes reais |
7. sync/atomic: Operações atômicas
(1) Noções básicas sobre átomos
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64
// Atomic increment
newVal := atomic.AddInt64(&counter, 1)
fmt.Printf("AddInt64: %d\n", newVal)
// Atomic read
val := atomic.LoadInt64(&counter)
fmt.Printf("LoadInt64: %d\n", val)
// Atomic write
atomic.StoreInt64(&counter, 100)
// CAS (Compare And Swap)
swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
fmt.Printf("CAS: swapped=%v, val=%d\n", swapped, atomic.LoadInt64(&counter))
}
(2) ▶ Exemplo: Contadores atômicos x mutexes
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type AtomicCounter struct {
value int64
}
func (c *AtomicCounter) Increment() {
atomic.AddInt64(&c.value, 1)
}
func (c *AtomicCounter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
type MutexCounter struct {
mu sync.Mutex
value int
}
func (c *MutexCounter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *MutexCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
n := 100000
// Atomic counter
atomicStart := time.Now()
var ac AtomicCounter
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ac.Increment()
}()
}
wg.Wait()
fmt.Printf("Atomic: %d (%v)\n", ac.Value(), time.Since(atomicStart))
// Mutex counter
muStart := time.Now()
var mc MutexCounter
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mc.Increment()
}()
}
wg.Wait()
fmt.Printf("Mutex: %d (%v)\n", mc.Value(), time.Since(muStart))
}
(3) Funções atômicas comuns
| Operação | Função | Finalidade |
|---|---|---|
| Somar/Subtrair | AddInt64, AddUint32 |
Contador |
| Leitura | LoadInt64, LoadPointer |
Leitura segura |
| Gravar | StoreInt64, StorePointer |
Gravação segura |
| CAS | CompareAndSwapInt64 |
Bloqueio otimista |
| Troca | SwapInt64 |
Troca atômica |
8. detector de corrida
(1) ▶ Exemplo: Detecção de conflito de acesso
// race_example.go
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // Data race!
}()
}
wg.Wait()
fmt.Println(counter)
}
$ go run -race race_example.go
==================
WARNING: DATA RACE
Read at 0x00c0000... by goroutine X
main.main.func1()
race_example.go:14 +0x...
Previous write at 0x00c0000... by goroutine Y
main.main.func1()
race_example.go:14 +0x...
==================
9. sync.Pool — Pool de objetos
(1) ▶ Exemplo: Pool de objetos
package main
import (
"fmt"
"sync"
)
type User struct {
Name string
Age int
}
var userPool = sync.Pool{
New: func() interface{} {
return &User{}
},
}
func main() {
// Get from pool (avoids allocating a new object each time)
u := userPool.Get().(*User)
u.Name = "Alice"
u.Age = 28
fmt.Printf("Using: %+v\n", u)
// Put back into the pool after use
userPool.Put(u)
// Next Get reuses the previous object
u2 := userPool.Get().(*User)
fmt.Printf("Reusing: %+v\n", u2)
}
10. Exemplo completo: Contador de alta concorrência
// concurrent_counter.go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// ---------- 3 counter variants ----------
type Counter interface {
Increment()
Value() int64
}
// AtomicCounter
type AtomicCounter struct {
value int64
}
func (c *AtomicCounter) Increment() {
atomic.AddInt64(&c.value, 1)
}
func (c *AtomicCounter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
// MutexCounter
type MutexCounter struct {
mu sync.Mutex
value int64
}
func (c *MutexCounter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *MutexCounter) Value() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
// ChannelCounter
type ChannelCounter struct {
ch chan struct{}
value int64
done chan struct{}
}
func NewChannelCounter() *ChannelCounter {
c := &ChannelCounter{
ch: make(chan struct{}, 100),
done: make(chan struct{}),
}
go func() {
for range c.ch {
c.value++
}
close(c.done)
}()
return c
}
func (c *ChannelCounter) Increment() {
c.ch <- struct{}{}
}
func (c *ChannelCounter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
func (c *ChannelCounter) Close() {
close(c.ch)
<-c.done
}
// ---------- Benchmark ----------
func benchmarkCounter(c Counter, goroutines, increments int) time.Duration {
var wg sync.WaitGroup
start := time.Now()
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
c.Increment()
}
}()
}
wg.Wait()
return time.Since(start)
}
func main() {
const (
goroutines = 100
increments = 10000
)
fmt.Printf("Benchmark: %d goroutines, each executing %d Increments\n\n", goroutines, increments)
// 1. Atomic counter
ac := &AtomicCounter{}
atomicTime := time.Now()
atomicDone := make(chan struct{})
go func() {
defer close(atomicDone)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
ac.Increment()
}
}()
}
wg.Wait()
}()
<-atomicDone
fmt.Printf("AtomicCounter: %d (expected %d) — %v\n",
ac.Value(), goroutines*increments, time.Since(atomicTime))
// 2. Mutex counter
mc := &MutexCounter{}
mutexTime := time.Now()
mutexDone := make(chan struct{})
go func() {
defer close(mutexDone)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
mc.Increment()
}
}()
}
wg.Wait()
}()
<-mutexDone
fmt.Printf("MutexCounter: %d (expected %d) — %v\n",
mc.Value(), goroutines*increments, time.Since(mutexTime))
// 3. Channel counter
cc := NewChannelCounter()
channelTime := time.Now()
channelDone := make(chan struct{})
go func() {
defer close(channelDone)
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < increments; i++ {
cc.Increment()
}
}()
}
wg.Wait()
cc.Close()
}()
<-channelDone
fmt.Printf("ChannelCounter: %d (expected %d) — %v\n",
cc.Value(), goroutines*increments, time.Since(channelTime))
fmt.Println("\nConclusion: Atomic > Mutex > Channel (performance from high to low)")
}
sequenceDiagram
participant G1 as Goroutine 1
participant G2 as Goroutine 2
participant G3 as Goroutine 3
participant M as Mutex
G1->>M: Lock()
Note over G1,M: G1 holds the lock
G2->>M: Lock() blocked
G3->>M: Lock() blocked
G1->>M: Unlock()
M-->>G2: wake up
M-->>G3: still blocked
G2->>M: Lock() success
Note over G2,M: G2 holds the lock
G2->>M: Unlock()
M-->>G3: wake up
G3->>M: Lock() success
*Counter).
❓ Perguntas Frequentes
P: Como escolho entre um Mutex e um RWMutex? R: Se as leituras forem muito mais numerosas que as gravações (>10:1) → RWMutex; se as leituras e as gravações forem aproximadamente iguais ou se as gravações forem mais numerosas que as leituras → Mutex. O RLock de um RWMutex permite leituras simultâneas, mas o próprio Lock mutualiza todas as leituras e gravações. Escolher a opção errada reduzirá o desempenho.
P: Como uso
sync.Once? R:sync.Once.Do(f)garante quefseja executado apenas uma vez, mesmo que 1.000 goroutines o chamem simultaneamente. Usos típicos: configurações de carregamento diferido, o padrão singleton e recursos que precisam ser inicializados apenas uma vez. Ele é implementado internamente por meio de operações atômicas e um mutex.
P: Em quais cenários o
sync.Mapé adequado? R: A documentação oficial recomenda dois cenários: (1) quando uma chave é gravada apenas uma vez, mas lida várias vezes (cache); (2) quando várias goroutines leem, gravam ou iteram sobre diferentes conjuntos de chaves. Fora isso, usar um mapa com um RWMutex é mais simples e oferece desempenho comparável.
P: Em quais cenários o
atomicé adequado? R: Contadores simples, sinalizadores de status e bloqueio otimista CAS (Compare And Swap). Oatomicé de 10 a 100 vezes mais rápido que um mutex, mas só pode ser usado com tipos básicos, comoint,uintepointer. Para lógicas complexas, use um mutex.
P: Como utilizo o detector de conflitos de acesso? R:
go run -race main.goougo test -race ./.... Ele detecta conflitos de acesso em tempo de execução — se duas goroutines acessarem simultaneamente a mesma variável e pelo menos uma delas estiver realizando uma operação de gravação, será emitido um aviso. Recomenda-se mantê-lo ativado para CI/CD.
P: Quando se deve usar
sync.Pool? R: Quando for necessário alocar e desalocar objetos temporários com frequência (como buffers para decodificação de JSON ou mensagens Protobuf). O pool armazena em cache os objetos alocados para reduzir a carga sobre o coletor de lixo. Observação: os objetos no pool podem ser recuperados pelo coletor de lixo a qualquer momento.
P: Um mutex é reentrante? R: Não. O
sync.Mutexdo Go é um bloqueio não reentrante — se a mesma goroutine chamar oLockduas vezes seguidas, isso resultará em um impasse. Se você precisar adquirir o bloqueio novamente na mesma goroutine, refatore o código para usarsync.Mutexcom chamadas recursivas ou usedeferpara garantir que o bloqueio seja liberado corretamente.
P: A ordem entre Lock e RLock é importante? R: É extremamente importante. O RWMutex não permite chamar Lock durante um RLock — isso causará um impasse. Também não permite chamar RLock durante um Lock (prioridade ao gravador). Recomendação: use apenas
Lockpara operações de gravação e apenasRLockpara operações de leitura; não as misture.
📖 Resumo
- Mutex:
Lock/Unlock, adequado para uma proporção de leitura e gravação de 1:1 - Bloqueio de leitura e gravação RWMutex:
RLockpara leituras simultâneas,Lockpara gravações exclusivas - sync.Once:
Do(f)garante que a função seja executada apenas uma vez - sync.Map: Um Map seguro para concorrência, destinado a cenários com muitas leituras e poucas gravações
- sincronização/atômica: contadores sem bloqueio, bloqueio otimista CAS
- detector de conflitos de acesso: O sinalizador
-racedetecta conflitos de acesso aos dados - sync.Pool: Um conjunto de objetos temporários que reduz a carga sobre o coletor de lixo
- Seleção: atomic > Mutex > RWMutex > Channel (em ordem decrescente de desempenho)
📝 Exercícios
-
Problema básico (Dificuldade ⭐): Use um mutex para proteger as leituras e gravações simultâneas em um
map[string]int. Inicie 10 goroutines de gravação para gravar dados e 10 goroutines de leitura para ler dados, e verifique se não há condição de corrida (o teste-raceé aprovado). -
Problema avançado (Dificuldade ⭐⭐): Compare o desempenho de quatro tipos de contadores (atômico, Mutex, RWMutex e canal) medindo o tempo gasto e verificando a correção de 100 goroutines que realizam 10.000 operações de incremento. Use
-racepara verificar se não há corridas de dados. -
Desafio (Dificuldade ⭐⭐⭐): Implemente um cache de alta concorrência com muitas leituras e poucas gravações. Requisitos: (1) Use RWMutex para proteção; (2) Ofereça suporte às operações Get/Set/Delete/Range; (3) Ofereça suporte à expiração por TTL (
time.Now().After(expireAt)); (4) Bloqueie todas as operações de leitura durante as operações de gravação, mas permita operações de leitura simultâneas; (5) Passe no teste-race.