Pacote sync
Lição 16: Pacote sync — O Guardião da Segurança de Concorrência
Analogia
Imagine um banheiro público:
- Mutex (exclusão mútua): A porta tem uma trava; quando uma pessoa entra, ela tranca a porta e os outros devem esperar. Apenas uma pessoa pode usar por vez.
- RWMutex (leitura-escrita): Uma sala de leitura de biblioteca — muitas pessoas podem ler simultaneamente (lock de leitura), mas se alguém quiser modificar materiais, deve esperar que todos os leitores saiam e ter acesso exclusivo (lock de escrita).
- Execução única (Once): Ao ingressar em uma empresa, um crachá de funcionário é emitido apenas uma vez. Não importa quantas vezes você peça, não será repetido após a primeira vez.
- Pool (pool de objetos): Cadeiras de sala de reunião — coloque-as de volta no armazenamento após o uso, traga-as novamente para a próxima reunião, em vez de comprar novas cada vez.
- sync.Map (mapa concorrente): Uma caixa de correio segura com múltiplas chaves; múltiplos carteiros podem entregar em diferentes compartimentos simultaneamente sem interferir uns nos outros.
Conceitos Fundamentais
1. Por que Precisamos do Pacote sync?
As goroutines do Go são leves e poderosas, mas quando múltiplas goroutines acessam dados compartilhados simultaneamente, ocorrem condições de corrida:
// Perigoso! Múltiplas goroutines modificando count simultaneamente
var count int
for i := 0; i < 1000; i++ {
go func() {
count++ // Corrida de dados!
}()
}
O pacote sync fornece primitivas de sincronização para garantir segurança de concorrência.
2. Visão Geral dos Tipos Principais
| Tipo | Propósito | Características |
|---|---|---|
sync.Mutex |
Exclusão mútua | Apenas uma goroutine pode mantê-la por vez |
sync.RWMutex |
Lock de leitura-escrita | Múltiplos leitores, escritor único; leitores não conflitam |
sync.WaitGroup |
Grupo de espera | Espera um grupo de goroutines completar (coberto na Lição 15) |
sync.Once |
Execução única | Garante que uma função execute apenas uma vez |
sync.Pool |
Pool de objetos | Reutiliza objetos, reduz alocações de memória |
sync.Map |
Mapa seguro para concorrência | Armazenamento key-value concorrente sem lock |
sync.Cond |
Variável de condição | Notificação de condição entre goroutines |
Pacote atomic |
Operações atômicas | Operações de concorrência seguras de nível inferior e mais eficientes |
Sintaxe Básica e Uso
💡 Mutex
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // Trava
count++ // Modifica variável compartilhada com segurança
mu.Unlock() // Destrava
}()
}
wg.Wait()
fmt.Println("count =", count) // Saída: count = 1000
}
defer mu.Unlock() garante que o lock seja liberado mesmo se a função causar panic, prevenindo deadlocks.
💡 RWMutex (Lock de Leitura-Escrita)
package main
import (
"fmt"
"sync"
)
func main() {
var rwmu sync.RWMutex
data := make(map[string]string)
var wg sync.WaitGroup
// Operação de escrita: usa lock de escrita
wg.Add(1)
go func() {
defer wg.Done()
rwmu.Lock() // Lock de escrita: exclusivo
data["key"] = "value"
rwmu.Unlock()
}()
// Operação de leitura: usa lock de leitura (pode ser concorrente)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
rwmu.RLock() // Lock de leitura: compartilhado
_ = data["key"]
rwmu.RUnlock()
}()
}
wg.Wait()
fmt.Println("data:", data)
}
RWMutex tem melhor desempenho; se as frequências de leitura e escrita são similares, Mutex é mais simples.
💡 Once (Execução Única)
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
var wg sync.WaitGroup
setup := func() {
fmt.Println("Inicialização (executa apenas uma vez)")
}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
once.Do(setup) // Apenas a primeira goroutine vai executar setup
fmt.Printf("goroutine %d concluída\n", id)
}(i)
}
wg.Wait()
}
Once garante que mesmo que múltiplas goroutines a chamem simultaneamente, a função fornecida só será executada uma vez. Comumente usada para inicialização de singleton.
💡 Pool (Pool de Objetos)
package main
import (
"fmt"
"sync"
)
func main() {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Criando novo objeto")
return make([]byte, 1024)
},
}
// Primeiro Get: chama New para criar
buf1 := pool.Get().([]byte)
fmt.Println("Objeto obtido, comprimento:", len(buf1))
// Retorna após uso
pool.Put(buf1)
// Segundo Get: reutiliza o objeto retornado anteriormente
buf2 := pool.Get().([]byte)
fmt.Println("Objeto reutilizado, comprimento:", len(buf2))
}
Pool podem ser coletados pelo garbage collector durante qualquer ciclo de GC. Não assuma que um objeto colocado com Put sempre pode ser recuperado com Get.
💡 Operações Atômicas
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var count int64 = 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&count, 1) // Incremento atômico
}()
}
wg.Wait()
fmt.Println("count =", atomic.LoadInt64(&count)) // Leitura atômica
}
atomic são mais leves que Mutex, adequadas para contadores simples, flags, etc.
💡 Detecção de Corrida
go run -race main.go # Detecta corridas em tempo de execução
go test -race ./... # Detecta corridas durante testes
-race durante o desenvolvimento; ajuda a descobrir problemas ocultos de corrida de dados.
Exemplos Práticos
Exemplo: Contador Thread-safe (Dificuldade ⭐)
Um contador seguro encapsulado com mutex:
package main
import (
"fmt"
"sync"
)
// SafeCounter é um contador thread-safe
type SafeCounter struct {
mu sync.Mutex
count int
}
// Inc incrementa o contador
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
// Value retorna o valor atual
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
// Inicia 100 goroutines, cada uma incrementando 100 vezes
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
counter.Inc()
}
}()
}
wg.Wait()
fmt.Println("Contagem final:", counter.Value()) // Saída: Contagem final: 10000
}
Exemplo: Cache Concorrente Seguro com sync.Map (Dificuldade ⭐⭐)
package main
import (
"fmt"
"sync"
)
// Cache é um cache concorrente seguro
type Cache struct {
store sync.Map
}
// Set armazena um valor no cache
func (c *Cache) Set(key string, value interface{}) {
c.store.Store(key, value)
}
// Get recupera um valor do cache
func (c *Cache) Get(key string) (interface{}, bool) {
c.store.Load(key)
}
// Delete remove uma chave do cache
func (c *Cache) Delete(key string) {
c.store.Delete(key)
}
// GetOrSet recupera ou define um valor atomicamente (evita computação redundante)
func (c *Cache) GetOrSet(key string, factory func() interface{}) interface{} {
if val, ok := c.store.Load(key); ok {
return val
}
// Usa LoadOrStore para garantir atomicidade
val, _ := c.store.LoadOrStore(key, factory())
return val
}
func main() {
cache := &Cache{}
var wg sync.WaitGroup
// Escritas concorrentes
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", id)
cache.Set(key, id*10)
fmt.Printf("Escrita: %s = %d\n", key, id*10)
}(i)
}
wg.Wait()
// Leituras concorrentes
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", id)
if val, ok := cache.Get(key); ok {
fmt.Printf("Leitura: %s = %v\n", key, val)
}
}(i)
}
wg.Wait()
// Usa GetOrSet para evitar computação redundante
result := cache.GetOrSet("computed", func() interface{} {
fmt.Println("Realizando computação complexa...")
return 42
})
fmt.Println("computed =", result)
// Obtém novamente, não vai recomputar
result2 := cache.GetOrSet("computed", func() interface{} {
fmt.Println("Esta linha não vai executar")
return 99
})
fmt.Println("computed =", result2)
}
Exemplo: Pool de Workers com Timeout (Uso Abrangente de sync) (Dificuldade ⭐⭐⭐)
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
)
// Task representa uma tarefa
type Task struct {
ID int
Data string
}
// Result representa um resultado
type Result struct {
TaskID int
Output string
WorkerID int
Duration time.Duration
}
// WorkerPool é um pool de workers
type WorkerPool struct {
workerCount int
taskCh chan Task
resultCh chan Result
wg sync.WaitGroup
processed int64 // contador atômico
errors int64
once sync.Once // garante inicialização única
pool sync.Pool // reutiliza objetos Result
}
// NewWorkerPool cria um pool de workers
func NewWorkerPool(workerCount, taskBufferSize int) *WorkerPool {
wp := &WorkerPool{
workerCount: workerCount,
taskCh: make(chan Task, taskBufferSize),
resultCh: make(chan Result, taskBufferSize),
}
// Inicializa pool de objetos
wp.pool = sync.Pool{
New: func() interface{} {
return &Result{}
},
}
return wp
}
// Start inicia o pool de workers (executa apenas uma vez)
func (wp *WorkerPool) Start(ctx context.Context) {
wp.once.Do(func() {
for i := 0; i < wp.workerCount; i++ {
wp.wg.Add(1)
go wp.worker(ctx, i)
}
fmt.Printf("Pool de workers iniciado com %d workers\n", wp.workerCount)
})
}
// worker é a goroutine worker
func (wp *WorkerPool) worker(ctx context.Context, id int) {
defer wp.wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: sinal de saída recebido\n", id)
return
case task, ok := <-wp.taskCh:
if !ok {
fmt.Printf("Worker %d: channel de tarefas fechado\n", id)
return
}
// Obtém Result do pool
result := wp.pool.Get().(*Result)
result.TaskID = task.ID
result.WorkerID = id
// Simula processamento de tarefa
start := time.Now()
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
result.Output = fmt.Sprintf("Processado: %s", task.Data)
result.Duration = time.Since(start)
atomic.AddInt64(&wp.processed, 1)
wp.resultCh <- *result
// Retorna objeto Result ao pool (nota: envia uma cópia por valor)
wp.pool.Put(result)
}
}
}
// Submit submete uma tarefa
func (wp *WorkerPool) Submit(task Task) {
wp.taskCh <- task
}
// Close fecha o pool de workers
func (wp *WorkerPool) Close() {
close(wp.taskCh)
wp.wg.Wait()
close(wp.resultCh)
}
// Stats retorna estatísticas
func (wp *WorkerPool) Stats() (processed, errors int64) {
return atomic.LoadInt64(&wp.processed), atomic.LoadInt64(&wp.errors)
}
func main() {
rand.Seed(time.Now().UnixNano())
// Cria contexto com timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Cria pool de workers
pool := NewWorkerPool(3, 20)
pool.Start(ctx)
// Submete tarefas
var submitWg sync.WaitGroup
submitWg.Add(1)
go func() {
defer submitWg.Done()
for i := 1; i <= 10; i++ {
pool.Submit(Task{
ID: i,
Data: fmt.Sprintf("task-data-%d", i),
})
fmt.Printf("Tarefa #%d submetida\n", i)
}
}()
// Coleta resultados
var collectWg sync.WaitGroup
collectWg.Add(1)
go func() {
defer collectWg.Done()
for result := range pool.resultCh {
fmt.Printf("Tarefa#%d processada por Worker%d, levou %v -> %s\n",
result.TaskID, result.WorkerID, result.Duration, result.Output)
}
}()
// Espera submissão de tarefas completar
submitWg.Wait()
// Fecha pool de workers
pool.Close()
// Espera coleta de resultados completar
collectWg.Wait()
// Exibe estatísticas
processed, _ := pool.Stats()
fmt.Printf("\nEstatísticas: %d tarefas processadas no total\n", processed)
}
Casos de Uso Práticos
Caso 1: Limitação de Taxa para Requisições HTTP Concorrentes
Em crawlers ou chamadas de API, você precisa limitar a concorrência para evitar ser banido:
package main
import (
"fmt"
"sync"
"time"
)
// RateLimiter é um limitador de taxa de concorrência simples
type RateLimiter struct {
sem chan struct{}
mu sync.Mutex
active int
maxConc int
}
// NewRateLimiter cria um limitador de taxa; maxConc é a concorrência máxima
func NewRateLimiter(maxConc int) *RateLimiter {
return &RateLimiter{
sem: make(chan struct{}, maxConc),
maxConc: maxConc,
}
}
// Acquire adquire uma permissão
func (r *RateLimiter) Acquire() {
r.sem <- struct{}{}
r.mu.Lock()
r.active++
r.mu.Unlock()
}
// Release libera uma permissão
func (r *RateLimiter) Release() {
<-r.sem
r.mu.Lock()
r.active--
r.mu.Unlock()
}
// ActiveCount retorna a contagem ativa atual
func (r *RateLimiter) ActiveCount() int {
r.mu.Lock()
defer r.mu.Unlock()
return r.active
}
func main() {
limiter := NewRateLimiter(3) // máximo 3 concorrentes
var wg sync.WaitGroup
urls := []string{
"https://api.example.com/page1",
"https://api.example.com/page2",
"https://api.example.com/page3",
"https://api.example.com/page4",
"https://api.example.com/page5",
"https://api.example.com/page6",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
limiter.Acquire() // adquire permissão (bloqueia quando mais de 3)
defer limiter.Release() // libera permissão
fmt.Printf("[Concorrente:%d] Iniciando requisição: %s\n", limiter.ActiveCount(), u)
time.Sleep(time.Duration(100+len(u)*10) * time.Millisecond) // simula requisição
fmt.Printf("[Concorrente:%d] Requisição concluída: %s\n", limiter.ActiveCount(), u)
}(url)
}
wg.Wait()
fmt.Println("Todas as requisições concluídas")
}
Caso 2: Carregamento Preguiçoso de Configuração (Padrão Singleton)
Use sync.Once para garantir que a configuração seja carregada apenas uma vez:
package main
import (
"fmt"
"sync"
)
// Config é a configuração da aplicação
type Config struct {
DatabaseURL string
APIKey string
MaxRetries int
}
var (
config *Config
once sync.Once
)
// GetConfig obtém a configuração (carregamento preguiçoso, inicializa apenas uma vez)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("Carregando configuração (executa apenas uma vez)...")
// Simula carregamento de config de arquivo ou variáveis de ambiente
config = &Config{
DatabaseURL: "postgres://localhost:5432/mydb",
APIKey: "sk-xxxxxxxxxxxx",
MaxRetries: 3,
}
})
return config
}
func main() {
var wg sync.WaitGroup
// Múltiplas goroutines obtendo config simultaneamente
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cfg := GetConfig()
fmt.Printf("Goroutine %d: DB=%s, Retries=%d\n",
id, cfg.DatabaseURL, cfg.MaxRetries)
}(i)
}
wg.Wait()
fmt.Println("Carregamento de configuração concluído")
}
❓ Perguntas Frequentes
P1: Como escolher entre Mutex e RWMutex?
| Cenário | Recomendação |
|---|---|
| Muita leitura, pouca escrita (ex: cache) | RWMutex — múltiplas leituras podem ser concorrentes |
| Leitura/escrita balanceada ou muita escrita | Mutex — mais simples, menor overhead |
| Contador simples | atomic — mais leve |
// ❌ Usando Mutex para cenários de muita leitura, pouca escrita — desempenho ruim
var mu sync.Mutex
mu.Lock()
val := cache[key] // operação de leitura também exclusiva
mu.Unlock()
// ✅ Use RWMutex — operações de leitura podem ser concorrentes
var rwmu sync.RWMutex
rwmu.RLock()
val := cache[key] // múltiplas goroutines podem ler simultaneamente
rwmu.RUnlock()
P2: Como escolher entre sync.Map e um mapa com lock?
// Cenário 1: Pares key-value são relativamente estáticos, muita leitura, pouca escrita -> sync.Map
var m sync.Map
m.Store("key", "value")
val, _ := m.Load("key")
// Cenário 2: Adição/exclusão frequente, precisa de iteração -> mapa com lock
type SafeMap struct {
mu sync.RWMutex
m map[string]string
}
// sync.Map é adequado para:
// 1. Chaves são escritas uma vez mas lidas muitas vezes (ex: cache)
// 2. Múltiplas goroutines leem/escrevem chaves diferentes (sem sobreposição)
P3: Quando objetos no Pool são coletados pelo garbage collector?
Objetos no Pool podem ser limpos durante qualquer ciclo de GC. Não use Pool como armazenamento de longo prazo:
// ❌ Uso errado: usando Pool como cache
pool := &sync.Pool{New: func() interface{} { return expensiveObject() }}
obj := pool.Get()
// ... usa
pool.Put(obj)
// Após o próximo GC, obj pode ter sido coletado
// ✅ Uso correto: reutiliza objetos temporários, reduz alocações
pool := &sync.Pool{
New: func() interface{} {
return make([]byte, 0, 4096) // pré-aloca buffer
},
}
buf := pool.Get().([]byte)[:0] // obtém e reseta
// ... usa buf
pool.Put(buf) // retorna
P4: Como evitar deadlocks?
// ❌ Deadlock: mesma goroutine trava duas vezes
var mu sync.Mutex
mu.Lock()
mu.Lock() // Bloqueia para sempre! Deadlock!
// ✅ Solução 1: Use defer para garantir liberação
mu.Lock()
defer mu.Unlock()
// ... libera mesmo em caso de panic
// ✅ Solução 2: Preste atenção na ordem dos locks, evite locks cruzados
// Duas goroutines: uma mantém lock A esperando por B, a outra mantém B esperando por A -> deadlock
// Solução: sempre bloqueie na ordem A->B
// ✅ Solução 3: Use locks com timeout (Go 1.18+ recomenda usar context)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Use channel com select para controle de timeout
📖 Resumo
| Conceito | Pontos-chave |
|---|---|
sync.Mutex |
Exclusão mútua, apenas uma goroutine pode manter por vez |
sync.RWMutex |
Lock de leitura-escrita, múltiplas leituras, escrita única; ideal para cenários com muita leitura |
sync.Once |
Garante que função execute apenas uma vez; adequado para singletons/inicialização |
sync.Pool |
Pool de objetos, reutiliza objetos temporários, reduz pressão do GC |
sync.Map |
Mapa concorrente seguro; melhor que mapa com lock em cenários específicos |
atomic |
Operações atômicas; solução de concorrência segura mais leve |
-race |
Detector de corrida; deve ser habilitado durante desenvolvimento |
| Prevenção de deadlock | defer Unlock, ordem consistente de locks, evite locks aninhados |
Princípios de Seleção:
- Contador/flag simples →
atomic - Exclusão mútua geral →
Mutex - Muita leitura, pouca escrita →
RWMutex - Inicialização única →
Once - Reutilização de objetos →
Pool - Leitura/escrita concorrente de key-value →
sync.Map
📝 Exercícios
Exercício 1: Implementar uma Pilha Thread-safe
Implemente uma pilha concorrente segura (LIFO) suportando operações Push, Pop e Size.
package main
import (
"errors"
"fmt"
"sync"
)
// ThreadSafeStack é uma pilha concorrente segura
type ThreadSafeStack struct {
// Seu código:
// - Escolha estrutura de dados apropriada
// - Escolha primitiva de sincronização apropriada
}
// NewStack cria uma nova pilha
func NewStack() *ThreadSafeStack {
// Seu código
return nil
}
// Push empilha um valor na pilha
func (s *ThreadSafeStack) Push(val int) {
// Seu código
}
// Pop desempilha um valor da pilha (retorna erro quando vazia)
func (s *ThreadSafeStack) Pop() (int, error) {
// Seu código
return 0, errors.New("pilha vazia")
}
// Size retorna o número de elementos na pilha
func (s *ThreadSafeStack) Size() int {
// Seu código
return 0
}
func main() {
stack := NewStack()
var wg sync.WaitGroup
// Push concorrente
for i := 0; i < 100; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
stack.Push(val)
}(i)
}
wg.Wait()
fmt.Println("Tamanho da pilha:", stack.Size()) // Deve imprimir 100
// Pop concorrente
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if val, err := stack.Pop(); err == nil {
fmt.Println("Desempilhado:", val)
}
}()
}
wg.Wait()
fmt.Println("Tamanho restante:", stack.Size()) // Deve imprimir 50
}
Exercício 2: Implementar Cache com TTL
Use sync.RWMutex para implementar um cache com suporte a TTL (time-to-live):
package main
import (
"fmt"
"sync"
"time"
)
// TTLCache é um cache com tempo de expiração
type TTLCache struct {
// Seu código:
// - Estrutura de armazenamento
// - Lock de sincronização
// - Configuração de TTL
}
// NewTTLCache cria um cache; ttl é o tempo de expiração
func NewTTLCache(ttl time.Duration) *TTLCache {
// Seu código
return nil
}
// Set define um par key-value
func (c *TTLCache) Set(key string, value interface{}) {
// Seu código
}
// Get recupera um valor (retorna false se expirado)
func (c *TTLCache) Get(key string) (interface{}, bool) {
// Seu código
return nil, false
}
// Delete exclui uma chave
func (c *TTLCache) Delete(key string) {
// Seu código
}
// Size retorna o número de pares key-value válidos
func (c *TTLCache) Size() int {
// Seu código
return 0
}
func main() {
cache := NewTTLCache(2 * time.Second)
cache.Set("name", "Linguagem Go")
cache.Set("version", "1.21")
if val, ok := cache.Get("name"); ok {
fmt.Println("name =", val)
}
fmt.Println("Tamanho do cache:", cache.Size())
time.Sleep(3 * time.Second)
if _, ok := cache.Get("name"); !ok {
fmt.Println("name expirou")
}
fmt.Println("Tamanho após expiração:", cache.Size())
}
Exercício 3: Map-Reduce Paralelo
Use o pacote sync para implementar um Map-Reduce paralelo simples:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// ParallelMapReduce processa dados em paralelo e agrega resultados
// Parâmetros:
// - data: slice de dados de entrada
// - mapFn: função de mapeamento, converte entrada em um valor
// - reduceFn: função de redução, mescla dois resultados
//
// Requisitos:
// - Divida dados em segmentos, cada um processado por uma goroutine
// - Use sync.WaitGroup para esperar todas as goroutines completarem
// - Use sync.Mutex para proteger o resultado da redução
// - Use atomic para contar total de elementos processados
func ParallelMapReduce(
data []int,
mapFn func(int) int,
reduceFn func(int, int) int,
) int {
// Seu código:
// 1. Determine contagem de segmentos (sugere 4 segmentos)
// 2. Lance goroutines para processar cada segmento
// 3. mapFn processa cada elemento
// 4. reduceFn mescla resultados dos segmentos
// 5. Retorne resultado final
return 0
}
func main() {
data := make([]int, 100)
for i := range data {
data[i] = i + 1 // 1 a 100
}
// Computa a soma dos quadrados de todos os elementos
result := ParallelMapReduce(
data,
func(x int) int { return x * x }, // map: quadrado
func(a, b int) int { return a + b }, // reduce: soma
)
fmt.Println("1² + 2² + 3² + ... + 100² =", result)
// Saída esperada: 338350
}
Próxima Lição
Tendo completado o pacote sync, você dominou as ferramentas centrais da programação concorrente em Go. Em seguida, consolidaremos o que aprendemos através de exercícios abrangentes.



