Channels são elegantes para comunicação entre goroutines, mas nem toda situação de concorrência envolve comunicação. Às vezes o problema é simplesmente: esperar que um grupo de goroutines termine, ou proteger uma estrutura de dados compartilhada de acesso simultâneo.
Para esses casos, o pacote sync oferece primitivas de sincronização clássicas — WaitGroup, Mutex, RWMutex, Once e Map — implementadas de forma eficiente e idiomática no Go. Elas não substituem channels, complementam. A escolha entre channels e sync depende do problema: channels para comunicação e transferência de dados, sync para coordenação de acesso a estado compartilhado.
sync.WaitGroup: aguardando grupos de goroutines
WaitGroup resolve o problema de esperar que N goroutines terminem antes de prosseguir. Três métodos formam sua API completa:
Add(n int)— incrementa o contador em nDone()— decrementa o contador em 1Wait()— bloqueia até o contador chegar a zero
package main
import (
"fmt"
"sync"
"time"
)
func processarArquivo(nome string, wg *sync.WaitGroup) {
defer wg.Done() // garante decremento mesmo se a função entrar em pânico
fmt.Printf("iniciando: %s\n", nome)
time.Sleep(100 * time.Millisecond) // simula processamento
fmt.Printf("concluído: %s\n", nome)
}
func main() {
arquivos := []string{
"relatorio_jan.csv",
"relatorio_fev.csv",
"relatorio_mar.csv",
"relatorio_abr.csv",
}
var wg sync.WaitGroup
for _, arquivo := range arquivos {
wg.Add(1)
go processarArquivo(arquivo, &wg)
}
wg.Wait()
fmt.Println("todos os arquivos processados")
}
Três regras críticas para usar WaitGroup corretamente:
Sempre passe por ponteiro. WaitGroup não deve ser copiado após o primeiro uso. Passar por valor copia o estado interno e quebra a sincronização.
Chame Add antes de go. Se Add for chamado dentro da goroutine, pode não executar antes do Wait, causando retorno prematuro.
Use defer wg.Done() na primeira linha da goroutine. Isso garante que Done seja chamado mesmo em caso de pânico ou retorno antecipado.
WaitGroup com coleta de resultados
O padrão mais comum combina WaitGroup com um channel para coletar resultados:
package main
import (
"fmt"
"sync"
)
type Resultado struct {
Arquivo string
Linhas int
Erro error
}
func contarLinhas(arquivo string, wg *sync.WaitGroup, resultados chan<- Resultado) {
defer wg.Done()
// Simula contagem
linhas := len(arquivo) * 10
resultados <- Resultado{Arquivo: arquivo, Linhas: linhas}
}
func main() {
arquivos := []string{"dados.csv", "usuarios.csv", "pedidos.csv"}
resultados := make(chan Resultado, len(arquivos))
var wg sync.WaitGroup
for _, arquivo := range arquivos {
wg.Add(1)
go contarLinhas(arquivo, &wg, resultados)
}
// Fecha o channel quando todas as goroutines terminarem
go func() {
wg.Wait()
close(resultados)
}()
total := 0
for r := range resultados {
if r.Erro != nil {
fmt.Printf("erro em %s: %v\n", r.Arquivo, r.Erro)
continue
}
fmt.Printf("%-20s %d linhas\n", r.Arquivo, r.Linhas)
total += r.Linhas
}
fmt.Printf("total: %d linhas\n", total)
}
sync.Mutex: exclusão mútua
Um Mutex (mutual exclusion lock) garante que apenas uma goroutine execute uma seção crítica por vez. Quando uma goroutine chama Lock(), as demais que também chamarem Lock() bloquearão até que Unlock() seja chamado:
package main
import (
"fmt"
"sync"
)
type Contador struct {
mu sync.Mutex
valor int
}
func (c *Contador) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock()
c.valor++
}
func (c *Contador) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
func main() {
contador := &Contador{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contador.Incrementar()
}()
}
wg.Wait()
fmt.Println("valor final:", contador.Valor()) // sempre 1000
}
Sem o mutex, múltiplas goroutines poderiam ler o valor atual, incrementar localmente e escrever de volta simultaneamente — causando perdas de incremento. Com go test -race, esse tipo de problema é detectado automaticamente.
O defer c.mu.Unlock() imediatamente após o Lock() é o padrão idiomático — garante que o lock seja liberado mesmo se a função retornar antecipadamente ou entrar em pânico.
sync.RWMutex: reads concorrentes, writes exclusivos
Quando um recurso é lido com muito mais frequência do que é escrito, RWMutex oferece melhor performance que Mutex. Múltiplas goroutines podem ler simultaneamente — mas um write exclusivo bloqueia todos os readers e writers:
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.RWMutex
dados map[string]string
}
func NovoCache() *Cache {
return &Cache{dados: make(map[string]string)}
}
func (c *Cache) Set(chave, valor string) {
c.mu.Lock() // write lock — exclusivo
defer c.mu.Unlock()
c.dados[chave] = valor
}
func (c *Cache) Get(chave string) (string, bool) {
c.mu.RLock() // read lock — concorrente com outros reads
defer c.mu.RUnlock()
valor, existe := c.dados[chave]
return valor, existe
}
func (c *Cache) Delete(chave string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.dados, chave)
}
func (c *Cache) Tamanho() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.dados)
}
func main() {
cache := NovoCache()
var wg sync.WaitGroup
// 10 writers
for i := 0; i < 10; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
cache.Set(fmt.Sprintf("chave%d", i), fmt.Sprintf("valor%d", i))
}()
}
// 100 readers concorrentes
for i := 0; i < 100; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
cache.Get(fmt.Sprintf("chave%d", i%10))
}()
}
wg.Wait()
fmt.Println("tamanho do cache:", cache.Tamanho())
}
sync.Once: inicialização garantidamente única
sync.Once garante que uma função seja executada exatamente uma vez, independentemente de quantas goroutines a chamem. É o mecanismo ideal para inicialização lazy e segura:
package main
import (
"fmt"
"sync"
)
type ConexaoDB struct {
host string
}
var (
instanciaDB *ConexaoDB
once sync.Once
)
func obterConexao() *ConexaoDB {
once.Do(func() {
fmt.Println("inicializando conexão com o banco...")
// Operação custosa — executada apenas uma vez
instanciaDB = &ConexaoDB{host: "localhost:5432"}
fmt.Println("conexão inicializada")
})
return instanciaDB
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
conn := obterConexao()
fmt.Println("usando conexão:", conn.host)
}()
}
wg.Wait()
// "inicializando conexão com o banco..." aparece apenas uma vez
}
sync.Once é thread-safe por design. Mesmo que 100 goroutines chamem once.Do simultaneamente, a função passada executa apenas uma vez — as demais aguardam a conclusão antes de prosseguir.
sync.Map: map thread-safe
O map nativo do Go não é seguro para uso concorrente. Para casos onde um map precisa ser acessado por múltiplas goroutines, sync.Map oferece uma implementação thread-safe otimizada para dois casos específicos: entradas escritas uma vez e lidas muitas vezes, e goroutines trabalhando em conjuntos de chaves disjuntos.
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// Escrita concorrente
for i := 0; i < 5; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
m.Store(fmt.Sprintf("usuario_%d", i), i*100)
}()
}
wg.Wait()
// Leitura
valor, existe := m.Load("usuario_3")
if existe {
fmt.Println("usuario_3:", valor)
}
// LoadOrStore — carrega se existir, armazena se não existir
anterior, carregado := m.LoadOrStore("usuario_5", 500)
fmt.Printf("carregado=%v valor=%v\n", carregado, anterior)
// Delete
m.Delete("usuario_0")
// Iteração — ordem não garantida
m.Range(func(chave, valor any) bool {
fmt.Printf("%v → %v\n", chave, valor)
return true // retornar false interrompe a iteração
})
}
Para a maioria dos casos de uso, um map protegido por sync.RWMutex é mais previsível e performático. sync.Map tem casos de uso específicos — não é um substituto geral para maps com mutex.
sync.Pool: reuso de objetos alocados
sync.Pool é um pool de objetos temporários que podem ser reutilizados entre goroutines, reduzindo a pressão sobre o garbage collector:
package main
import (
"bytes"
"fmt"
"sync"
)
var poolBuffers = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func processarRequisicao(dados string) string {
// Obtém buffer do pool em vez de alocar novo
buf := poolBuffers.Get().(*bytes.Buffer)
buf.Reset() // limpa o buffer antes de usar
defer poolBuffers.Put(buf) // devolve ao pool ao terminar
buf.WriteString("processado: ")
buf.WriteString(dados)
return buf.String()
}
func main() {
var wg sync.WaitGroup
resultados := make([]string, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
resultados[i] = processarRequisicao(fmt.Sprintf("dados_%d", i))
}()
}
wg.Wait()
for _, r := range resultados {
fmt.Println(r)
}
}
O pool é ideal para objetos grandes e frequentemente alocados — buffers de bytes, conexões temporárias, estruturas de parsing. Objetos no pool podem ser coletados pelo GC a qualquer momento, então nunca armazene estado importante neles.
Armadilhas comuns com sync
Copiar Mutex. Um Mutex nunca deve ser copiado após o primeiro uso. Sempre use ponteiro para structs que contêm mutex:
// ERRADO — copia o mutex
func (c Contador) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
// CORRETO — receiver por ponteiro
func (c *Contador) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
Deadlock por lock dentro de lock. Tentar adquirir um Mutex que já está locked pela mesma goroutine causa deadlock imediato — Go não suporta locks reentrantes:
func (c *Contador) metodoA() {
c.mu.Lock()
defer c.mu.Unlock()
c.metodoB() // DEADLOCK — metodoB tenta lock novamente
}
func (c *Contador) metodoB() {
c.mu.Lock() // bloqueia para sempre
defer c.mu.Unlock()
}
Lock por tempo excessivo. Manter um lock enquanto realiza I/O ou operações lentas bloqueia todas as outras goroutines que precisam do mesmo lock. Minimize o código dentro de seções críticas:
// ERRADO — I/O dentro do lock
func (c *Cache) AtualizarComDB(chave string) {
c.mu.Lock()
defer c.mu.Unlock()
dados := consultarBancoDados(chave) // bloqueia todas as outras goroutines
c.dados[chave] = dados
}
// CORRETO — I/O fora do lock
func (c *Cache) AtualizarComDB(chave string) {
dados := consultarBancoDados(chave) // sem lock durante I/O
c.mu.Lock()
defer c.mu.Unlock()
c.dados[chave] = dados
}
Exemplo completo: worker pool
Combinando WaitGroup, Mutex e channels em um worker pool — um dos padrões mais usados em Go para processamento paralelo com controle de concorrência:
package main
import (
"fmt"
"sync"
"time"
)
type WorkerPool struct {
tarefas chan func()
wg sync.WaitGroup
resultados []string
mu sync.Mutex
}
func NovoWorkerPool(workers int) *WorkerPool {
pool := &WorkerPool{
tarefas: make(chan func(), 100),
}
for i := 0; i < workers; i++ {
pool.wg.Add(1)
i := i
go func() {
defer pool.wg.Done()
for tarefa := range pool.tarefas {
tarefa()
fmt.Printf("worker %d: tarefa concluída\n", i)
}
}()
}
return pool
}
func (p *WorkerPool) Enviar(tarefa func()) {
p.tarefas <- tarefa
}
func (p *WorkerPool) AdicionarResultado(r string) {
p.mu.Lock()
defer p.mu.Unlock()
p.resultados = append(p.resultados, r)
}
func (p *WorkerPool) Aguardar() []string {
close(p.tarefas)
p.wg.Wait()
return p.resultados
}
func main() {
pool := NovoWorkerPool(3)
for i := 1; i <= 9; i++ {
i := i
pool.Enviar(func() {
time.Sleep(100 * time.Millisecond)
pool.AdicionarResultado(fmt.Sprintf("item_%d processado", i))
})
}
resultados := pool.Aguardar()
fmt.Printf("\n%d resultados coletados\n", len(resultados))
}
Resumo do que foi coberto
Este artigo apresentou as primitivas do pacote sync em profundidade: WaitGroup para aguardar grupos de goroutines, Mutex para exclusão mútua de seções críticas, RWMutex para reads concorrentes com writes exclusivos, Once para inicialização única garantida, Map para maps thread-safe e Pool para reuso de objetos. As armadilhas mais comuns — cópia de mutex, deadlock reentrante e locks longos — foram identificadas com padrões de correção. O próximo artigo apresenta o pacote context em profundidade.
Referências e leituras complementares
-
Documentação do pacote sync — Referência completa de todas as primitivas. https://pkg.go.dev/sync
-
Go by Example: Mutexes — Exemplos práticos de Mutex e RWMutex. https://gobyexample.com/mutexes
-
Go by Example: WaitGroups — Exemplos práticos de WaitGroup. https://gobyexample.com/waitgroups
-
Go Blog: Share Memory by Communicating — Filosofia de concorrência do Go. https://go.dev/blog/codelab-share
-
Go Race Detector — Documentação do detector de race conditions. https://go.dev/doc/articles/race_detector
-
Uber Go Style Guide — Concurrency — Boas práticas de concorrência adotadas na Uber. https://github.com/uber-go/guide/blob/master/style.md#concurrency