<h2>Entendendo Goroutines e a Necessidade de Sincronização</h2>
<p>Go foi projetado com concorrência em mente. As goroutines são funções executadas de forma leve e independente, permitindo que você crie centenas de milhares delas sem sobrecarregar o sistema. Contudo, quando múltiplas goroutines precisam trabalhar juntas ou acessar recursos compartilhados, surge um problema fundamental: como garantir que todas terminem suas tarefas antes de continuar? Como evitar condições de corrida e estados inconsistentes?</p>
<p>É aqui que entra o pacote <code>sync</code> da biblioteca padrão do Go. Dois primitivos se destacam por sua utilidade: <code>sync.WaitGroup</code> para coordenar múltiplas goroutines em paralelo e <code>sync.Once</code> para garantir que uma operação execute exatamente uma vez. Ambos são essenciais para escrever código concorrente robusto e previsível.</p>
<h2>sync.WaitGroup: Coordenação de Múltiplas Tarefas</h2>
<h3>Conceito Fundamental</h3>
<p><code>sync.WaitGroup</code> é um contador que permite que você aguarde a conclusão de um conjunto de goroutines. Funciona como um semáforo simples: você adiciona tarefas ao grupo, marca cada uma como concluída quando termina e bloqueia a execução até que todas terminem. Internamente, mantém um contador: cada chamada a <code>Add()</code> incrementa, cada chamada a <code>Done()</code> decrementa, e <code>Wait()</code> bloqueia até que o contador chegue a zero.</p>
<p>A beleza dessa abordagem é que você não precisa saber antecipadamente quantas goroutines terá — pode adicionar dinamicamente. Além disso, é segura para concorrência: o WaitGroup usa um mutex interno para garantir que o contador seja modificado de forma atômica.</p>
<h3>Estrutura e Métodos</h3>
<p>O <code>sync.WaitGroup</code> possui três métodos principais:</p>
<ul>
<li><strong><code>Add(delta int)</code></strong>: Adiciona <code>delta</code> ao contador interno. Você chama isso antes de iniciar uma goroutine.</li>
<li><strong><code>Done()</code></strong>: Decrementa o contador em 1. Equivalente a <code>Add(-1)</code>. Você chama isso quando uma goroutine termina.</li>
<li><strong><code>Wait()</code></strong>: Bloqueia até que o contador chegue a zero. Isso é chamado no código principal para esperar todas as goroutines.</li>
</ul>
<h3>Exemplo Prático: Processamento Paralelo de Dados</h3>
<pre><code class="language-go">package main
import (
"fmt"
"sync"
"time"
)
func processItem(id int, wg *sync.WaitGroup) {
// Garante que Done() será chamado ao final
defer wg.Done()
// Simula algum processamento
fmt.Printf("Iniciando processamento do item %d\n", id)
time.Sleep(time.Duration(id) * time.Second)
fmt.Printf("Concluído o item %d\n", id)
}
func main() {
var wg sync.WaitGroup
// Processaremos 5 itens em paralelo
for i := 1; i <= 5; i++ {
wg.Add(1) // Adiciona 1 ao contador
go processItem(i, &wg)
}
// Bloqueia até que todas as goroutines terminem
wg.Wait()
fmt.Println("Todos os itens foram processados!")
}</code></pre>
<p>Quando você executa este código, verá que os itens são processados em paralelo. O <code>Wait()</code> no final garante que o programa não saia até que todas as goroutines terminem. Sem ele, o programa encerraria antes das goroutines completarem.</p>
<h3>Armadilhas Comuns</h3>
<p>A primeira armadilha é chamar <code>Add()</code> dentro de uma goroutine que já foi iniciada — isso cria uma condição de corrida onde <code>Wait()</code> pode retornar antes da adição ser registrada. Sempre chame <code>Add()</code> no código principal antes de iniciar a goroutine.</p>
<p>A segunda é esquecer de chamar <code>Done()</code>. Se você tiver 5 tarefas, chamar <code>Add(5)</code> mas apenas 4 <code>Done()</code>, o programa ficará pendurado indefinidamente esperando algo que nunca chegará. Use <code>defer wg.Done()</code> para garantir que seja chamado mesmo em caso de pânico.</p>
<h2>sync.Once: Executando Código Exatamente Uma Vez</h2>
<h3>Quando Você Precisa de Once</h3>
<p><code>sync.Once</code> resolve um problema diferente: você tem uma operação que deve executar exatamente uma vez, mesmo que múltiplas goroutines tentem iniciá-la simultaneamente. Exemplos clássicos incluem inicializar uma conexão com banco de dados, carregar configurações, criar um logger singleton ou validar licenças. Na maioria desses casos, fazer a operação múltiplas vezes desperdiça recursos ou causa comportamentos inesperados.</p>
<p>O <code>sync.Once</code> usa um padrão interno de "double-checked locking" otimizado, garantindo que mesmo sob contenção alta, a função é executada apenas uma vez. Depois disso, qualquer chamada a <code>Do()</code> retorna imediatamente.</p>
<h3>Estrutura e Método</h3>
<p>O <code>sync.Once</code> possui um único método:</p>
<ul>
<li><strong><code>Do(f func())</code></strong>: Executa a função <code>f</code> exatamente uma vez. Se já foi executado, ignora a chamada e retorna.</li>
</ul>
<h3>Exemplo Prático: Inicialização de Recurso Compartilhado</h3>
<pre><code class="language-go">package main
import (
"fmt"
"sync"
)
type Database struct {
connection string
}
var (
dbInstance *Database
once sync.Once
)
func getDatabase() *Database {
once.Do(func() {
fmt.Println("Inicializando banco de dados...")
// Simula uma inicialização cara
dbInstance = &Database{
connection: "postgresql://localhost:5432/mydb",
}
})
return dbInstance
}
func main() {
var wg sync.WaitGroup
// Tenta inicializar o banco de dados de 10 goroutines diferentes
for i := 1; i <= 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
db := getDatabase()
fmt.Printf("Goroutine %d obteve conexão: %s\n", id, db.connection)
}(i)
}
wg.Wait()
}</code></pre>
<p>Se você executar este código, verá "Inicializando banco de dados..." aparecer apenas uma vez, mesmo com 10 goroutines tentando acessar simultaneamente. Cada uma recebe a mesma instância de banco de dados.</p>
<h3>Padrão Singleton Seguro</h3>
<p>Este é um padrão poderoso em Go. Diferente de linguagens como Java ou C#, Go não oferece um <code>singleton</code> nativo na linguagem. Com <code>sync.Once</code>, você implementa isso de forma segura e elegante:</p>
<pre><code class="language-go">package main
import (
"fmt"
"sync"
)
type Logger struct {
name string
}
var (
loggerInstance *Logger
loggerOnce sync.Once
)
func GetLogger() *Logger {
loggerOnce.Do(func() {
fmt.Println("Logger criado")
loggerInstance = &Logger{name: "GlobalLogger"}
})
return loggerInstance
}
func (l *Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.name, message)
}
func main() {
// Todas estas chamadas usam a mesma instância
logger1 := GetLogger()
logger2 := GetLogger()
logger1.Log("Primeira mensagem")
logger2.Log("Segunda mensagem")
fmt.Printf("São a mesma instância? %v\n", logger1 == logger2) // true
}</code></pre>
<h2>Combinando WaitGroup e Once: Um Caso de Uso Real</h2>
<h3>Cenário: Sistema de Cache com Inicialização Única</h3>
<p>Frequentemente, você precisa combinar ambos. Considere um cenário onde múltiplas goroutines devem enriquecer dados em um cache compartilhado, mas o cache deve ser inicializado apenas uma vez. Aqui está como estruturar isso:</p>
<pre><code class="language-go">package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
data map[string]string
}
var (
cache *Cache
cacheOnce sync.Once
)
func getCache() *Cache {
cacheOnce.Do(func() {
fmt.Println("Inicializando cache...")
cache = &Cache{
data: make(map[string]string),
}
})
return cache
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func populateCache(id int, key string, value string, wg *sync.WaitGroup) {
defer wg.Done()
cache := getCache()
time.Sleep(time.Duration(id100) time.Millisecond)
cache.Set(key, value)
fmt.Printf("Worker %d adicionou %s=%s\n", id, key, value)
}
func main() {
var wg sync.WaitGroup
// 5 workers adicionam dados ao cache
for i := 1; i <= 5; i++ {
wg.Add(1)
go populateCache(i, fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), &wg)
}
wg.Wait()
// Verifica o cache
c := getCache()
fmt.Println("\nConteúdo final do cache:")
for i := 1; i <= 5; i++ {
key := fmt.Sprintf("key%d", i)
fmt.Printf("%s => %s\n", key, c.Get(key))
}
}</code></pre>
<p>Neste exemplo, o <code>sync.Once</code> garante que o cache seja criado apenas uma vez, mesmo que múltiplas goroutines tentem acessá-lo simultaneamente. O <code>sync.WaitGroup</code> coordena o término de todas as goroutines que preenchem o cache. Finalmente, o <code>sync.RWMutex</code> dentro do cache protege os dados contra condições de corrida durante leitura e escrita.</p>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong><code>sync.WaitGroup</code> é essencial para coordenar múltiplas goroutines em paralelo</strong>, permitindo que o programa principal aguarde a conclusão de todas as tarefas sem desperdício de CPU ou deadlock. Use <code>Add()</code> antes de iniciar, <code>Done()</code> (via defer) quando terminar e <code>Wait()</code> para bloquear até o fim.</p>
<p>Compreendeu também que <strong><code>sync.Once</code> garante que uma operação execute exatamente uma vez</strong>, tornando-a ideal para inicialização singleton, configurações e recursos compartilhados caros. Chame <code>Do()</code> quantas vezes quiser — apenas a primeira execução ocorre. Juntas, essas duas primitivas resolvem a maioria dos problemas de sincronização em Go de forma elegante e eficiente.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://pkg.go.dev/sync#WaitGroup" target="_blank" rel="noopener noreferrer">Documentação oficial de sync.WaitGroup</a></li>
<li><a href="https://pkg.go.dev/sync#Once" target="_blank" rel="noopener noreferrer">Documentação oficial de sync.Once</a></li>
<li><a href="https://golang.org/doc/effective_go#concurrency" target="_blank" rel="noopener noreferrer">Effective Go - Concurrency</a></li>
<li><a href="https://www.gopl.io/" target="_blank" rel="noopener noreferrer">The Go Programming Language - Goroutines and Channels</a> (Capítulo 8)</li>
<li><a href="https://go.dev/blog/pipelines" target="_blank" rel="noopener noreferrer">Go Concurrency Patterns</a></li>
</ul>
<p><!-- FIM --></p>