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á


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:

GO
// 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

GO
// 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:

TEXT 📖 Somente leitura
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
💡 Dica: 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

GO
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)

GO
// 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
}
▶ Experimente
🔥 Erro comum: Após 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

GO
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

GO
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
▶ Experimente

(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

GO
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:

TEXT 📖 Somente leitura
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
💡 Dica: 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

GO
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

GO
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
▶ Experimente

(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

GO
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

GO 📖 Somente leitura
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))
}
56 linhas de lógica (limite de 40, somente leitura)

(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

GO
// 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)
}
▶ Experimente
BASH
$ 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

GO
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)
}
▶ Experimente

10. Exemplo completo: Contador de alta concorrência

GO
// 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)")
}
100%
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
🔥 Erro comum: o sync.Mutex não pode ser copiado. Se você passar uma estrutura que contenha um Mutex por valor (em vez de por ponteiro), o estado do Mutex será copiado — resultando em um comportamento inesperado de desbloqueio. Sempre passe-o por ponteiro (*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 que f seja 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). O atomic é de 10 a 100 vezes mais rápido que um mutex, mas só pode ser usado com tipos básicos, como int, uint e pointer. Para lógicas complexas, use um mutex.

P: Como utilizo o detector de conflitos de acesso? R: go run -race main.go ou go 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.Mutex do Go é um bloqueio não reentrante — se a mesma goroutine chamar o Lock duas vezes seguidas, isso resultará em um impasse. Se você precisar adquirir o bloqueio novamente na mesma goroutine, refatore o código para usar sync.Mutex com chamadas recursivas ou use defer para 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 Lock para operações de gravação e apenas RLock para operações de leitura; não as misture.


📖 Resumo


📝 Exercícios

  1. 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).

  2. 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 -race para verificar se não há corridas de dados.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%