Go: Padrões de concorrência em Go

Última atualização: 2026-08-26

A concorrência em Go não depende de bibliotecas, mas de padrões — Fan-in, Fan-out e Pipeline. Esses padrões, quando combinados, podem resolver 90% dos problemas de concorrência.

Quando você precisa processar grandes quantidades de dados — lendo-os de um gerador, fazendo com que vários workers os processem em paralelo e, por fim, unindo os resultados —, como é possível projetar uma solução elegante? Nesta lição, você vai dominar o padrão de concorrência mais clássico da comunidade Go.

1. Você aprenderá


2. A história real de um engenheiro de dados

(1) Problema: O processamento de 10 milhões de entradas de log em uma única thread levou a noite inteira

Fátima é engenheira de plataforma de dados. A empresa gera 10 milhões de registros de acesso por dia que precisam ser processados em tempo real:

“A primeira versão usava um loop ‘for’: ler um registro → analisá-lo → gravá-lo no banco de dados. No primeiro dia em que o sistema entrou em operação, percebemos que a velocidade de processamento não conseguia acompanhar o ritmo de produção — a fila de registros estava crescendo a uma taxa de 2 milhões de entradas por hora. Meu chefe disse: ‘Os relatórios de amanhã dependem desses dados’. Adicionei mais servidores, mas o código ainda era de thread único — a CPU estava usando apenas 5% de sua capacidade.”

O código dela na época:

GO
// Bad code: single-threaded, not utilizing CPU
func processLogs(logs []string) {
    for _, log := range logs {
        parsed := parse(log)       // CPU-intensive
        enriched := enrich(parsed) // CPU-intensive
        save(enriched)              // I/O-intensive
    }
    // 10 million log entries: 3 hours!
}

(2) A abordagem do Go à concorrência: pipeline + fan-out

GO
// pipeline.go
package main

import (
    "fmt"
    "sync"
    "time"
)

type LogEntry struct {
    Raw     string
    Level   string
    Message string
    Time    time.Time
}

func main() {
    logs := make([]string, 100)
    for i := range logs {
        logs[i] = fmt.Sprintf("log-%d", i)
    }
    start := time.Now()

    // Pipeline: generator → parse → enrich → save
    rawCh := generator(logs)
    parsedCh := parseStage(rawCh, 4)       // 4 parse workers
    enrichedCh := enrichStage(parsedCh, 4)  // 4 enrich workers
    saveStage(enrichedCh, 2)                // 2 save workers

    fmt.Printf("Processing complete, elapsed: %v\n", time.Since(start))
}

// Stage 1: Generator (data source)
func generator(logs []string) <-chan string {
    out := make(chan string, 100)
    go func() {
        defer close(out)
        for _, log := range logs {
            out <- log
        }
    }()
    return out
}

// Stage 2: Parse (Fan-out)
func parseStage(in <-chan string, workers int) <-chan LogEntry {
    out := make(chan LogEntry, 100)
    for i := 0; i < workers; i++ {
        go func() {
            for raw := range in {
                time.Sleep(1 * time.Millisecond) // Simulate parsing
                out <- LogEntry{Raw: raw, Level: "INFO", Message: raw, Time: time.Now()}
            }
        }()
    }
    return out
}

// Stage 3: Enrich (Fan-out)
func enrichStage(in <-chan LogEntry, workers int) <-chan LogEntry {
    out := make(chan LogEntry, 100)
    for i := 0; i < workers; i++ {
        go func() {
            for entry := range in {
                time.Sleep(2 * time.Millisecond) // Simulate data enrichment
                out <- entry
            }
        }()
    }
    return out
}

// Stage 4: Save (Fan-in to 2 workers)
func saveStage(in <-chan LogEntry, workers int) {
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for entry := range in {
                _ = entry // Simulate writing to database
                time.Sleep(500 * time.Microsecond)
            }
        }()
    }
    wg.Wait()
}

(3) Desempenho: Single-Threaded x Pipeline

Métrica Monothread Pipeline (4+4+2 trabalhadores) Melhoria
Tempo de processamento para 10 milhões de registros ~3 horas ~12 minutos 15x
Utilização da CPU 5% 85% 17x
Complexidade do código Simples Moderada
Escalabilidade Adicionar um servidor Adicionar um worker (alterar um número)

3. Pipeline

(1) ▶ Exemplo: Pipeline de processamento numérico

GO
package main

import (
    "fmt"
)

// Stage 1: Generate
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

// Stage 2: Square
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

// Stage 3: Double
func double(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * 2
        }
    }()
    return out
}

func main() {
    // Pipeline: generate → square → double
    for result := range double(square(generate(1, 2, 3, 4, 5))) {
        fmt.Println(result)
    }
    // Output: 2, 8, 18, 32, 50  (n²×2)
}
▶ Experimente
100%
graph LR
    A[generate] -->|chan int| B[square]
    B -->|chan int| C[double]
    C -->|chan int| D[main]
💡 Dica: O segredo de um pipeline é que cada etapa retorna um <-chan T (canal somente leitura) e aceita um <-chan T como entrada. Cada etapa é executada em sua própria goroutine e está conectada por meio de canais — esse é o modelo CSP do Go.


4. Fan-out / Fan-in

(1) ▶ Exemplo: Fan-out + Fan-in

GO 📖 Somente leitura
package main

import (
    "fmt"
    "sync"
)

// Stage 1: Generate
func generate(nums ...int) <-chan int {
    out := make(chan int, 10)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

// Stage 2: Worker (Fan-out target)
func worker(id int, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            result := n * n
            fmt.Printf("Worker %d: %d^2 = %d\n", id, n, result)
            out <- result
        }
    }()
    return out
}

// Fan-in: merge multiple channels
func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

func main() {
    // Fan-out: one generator distributes to 3 workers
    in := generate(1, 2, 3, 4, 5, 6)
    w1 := worker(1, in)
    w2 := worker(2, in)
    w3 := worker(3, in)

    // Fan-in: merge results from 3 workers into one channel
    results := fanIn(w1, w2, w3)

    for result := range results {
        fmt.Printf("Result: %d\n", result)
    }
}
55 linhas de lógica (limite de 40, somente leitura)
100%
graph LR
    G[Generate] -->|fan-out| W1[Worker 1]
    G -->|fan-out| W2[Worker 2]
    G -->|fan-out| W3[Worker 3]
    W1 -->|fan-in| R[Results]
    W2 -->|fan-in| R
    W3 -->|fan-in| R

5. Grupo de trabalhadores

(1) ▶ Exemplo: Grupo de trabalhadores

GO 📖 Somente leitura
package main

import (
    "fmt"
    "sync"
    "time"
)

type Job struct {
    ID      int
    Payload string
}

type Result struct {
    JobID  int
    Output string
    Err    error
}

func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job.ID)
        time.Sleep(100 * time.Millisecond) // Simulate work
        results <- Result{
            JobID:  job.ID,
            Output: fmt.Sprintf("processed by worker %d", id),
        }
    }
}

func main() {
    const numJobs = 20
    const numWorkers = 5

    jobs := make(chan Job, numJobs)
    results := make(chan Result, numJobs)

    // Start Worker Pool
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go worker(i, jobs, results, &wg)
    }

    // Send jobs
    for i := 0; i < numJobs; i++ {
        jobs <- Job{ID: i, Payload: fmt.Sprintf("data-%d", i)}
    }
    close(jobs)

    // Wait for all workers to complete
    wg.Wait()
    close(results)

    // Collect results
    for result := range results {
        fmt.Printf("Job %d → %s\n", result.JobID, result.Output)
    }
}
46 linhas de lógica (limite de 40, somente leitura)

(2) Pipeline x Pool de Trabalhadores x Fan-out/Fan-in

Padrão Conceito central Cenários aplicáveis
Pipeline Os dados passam por várias etapas, sendo que cada uma delas executa uma única ação Existem etapas de processamento claramente definidas (análise → transformação → salvamento)
Conjunto de trabalhadores Um número fixo de trabalhadores retira tarefas da fila de trabalhos Processamento em lote de tarefas (compressão de imagens, envio de e-mails)
Fan-out/Fan-in Distribuir para vários trabalhadores para processamento paralelo e, em seguida, mesclar os resultados Cálculo paralelo sem estado (operações numéricas, filtragem de dados)

6. Canal Or-Done

(1) ▶ Exemplo: Padrão “Or-Done”

GO 📖 Somente leitura
package main

import (
    "fmt"
    "time"
)

// orDone wraps a channel, making it support cancellation via a done channel
func orDone(done <-chan struct{}, c <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for {
            select {
            case <-done:
                return
            case v, ok := <-c:
                if !ok {
                    return
                }
                select {
                case out <- v:
                case <-done:
                    return
                }
            }
        }
    }()
    return out
}

func generateWithCancel(done <-chan struct{}, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case <-done:
                return
            case out <- n:
            }
        }
    }()
    return out
}

func main() {
    done := make(chan struct{})

    // Start generator (will be canceled after reaching 5)
    nums := generateWithCancel(done, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    // Consume safely via orDone
    for n := range orDone(done, nums) {
        fmt.Printf("Processing: %d\n", n)
        if n >= 5 {
            close(done) // Cancel all operations
        }
    }

    fmt.Println("Canceled")
    time.Sleep(100 * time.Millisecond) // Wait for goroutines to exit
}
53 linhas de lógica (limite de 40, somente leitura)
🔥 Erro comum: Deixar de consumir os dados do canal pode levar a vazamentos de goroutines — a goroutine geradora ficará bloqueada durante o envio. O padrão Or-Done garante que nenhuma goroutine seja perdida em caso de cancelamento. Envolva o canal com Or-Done quando tanto o remetente (upstream) quanto o consumidor (downstream) precisarem estar cientes do cancelamento.


7. Propagação de erros no errgroup

⚙️ Pré-requisito: Execute go get golang.org/x/sync/errgroup antes de usar este pacote.

(1) ▶ Exemplo: errgroup

GO
package main

import (
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func main() {
    // Create an errgroup
    // The first goroutine to return an error will cancel the others
    g := errgroup.Group{}

    // Start 3 goroutines
    for i := 0; i < 3; i++ {
        id := i
        g.Go(func() error {
            return doWork(id)
        })
    }

    // Wait for all goroutines to complete, get the first error
    if err := g.Wait(); err != nil {
        fmt.Printf("Task failed: %v\n", err)
    } else {
        fmt.Println("All succeeded")
    }
}

func doWork(id int) error {
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Duration(id+1) * 500 * time.Millisecond)

    if id == 1 {
        return fmt.Errorf("worker %d failed", id)
    }

    fmt.Printf("Worker %d complete\n", id)
    return nil
}
▶ Experimente

(2) ▶ Exemplo: errgroup + tempo limite de contexto

GO
package main

import (
    "context"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func main() {
    // errgroup with Context
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    g, ctx := errgroup.WithContext(ctx)

    // Workers check if Context is canceled
    for i := 0; i < 5; i++ {
        id := i
        g.Go(func() error {
            select {
            case <-time.After(time.Duration(id+1) * time.Second):
                fmt.Printf("Worker %d complete\n", id)
                return nil
            case <-ctx.Done():
                fmt.Printf("Worker %d canceled: %v\n", id, ctx.Err())
                return ctx.Err()
            }
        })
    }

    if err := g.Wait(); err != nil {
        fmt.Printf("errgroup stopped: %v\n", err)
    }
}
▶ Experimente

(3) sync.WaitGroup x errgroup

Propriedade sync.WaitGroup errgroup
Coleção de erros Não suportado ✅ Retornar o primeiro erro
Cancelar propagação Não suportado ✅ Cancelamento automático com WithContext
Gerenciamento de goroutines Adicionar/Concluir manualmente ✅ Automático via método do Go
Controle de tempo limite Implementação manual ✅ Fácil de implementar usando o contexto

8. Exemplo completo: Pipeline de processamento de dados em tempo real

GO
// data_pipeline.go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// ---------- Data types ----------

type DataPoint struct {
    ID        int
    Value     float64
    Timestamp time.Time
}

type ProcessedData struct {
    DataPoint
    Normalized bool
    Anomaly    bool
    Category   string
}

// ---------- Pipeline Stages ----------

// Stage 1: Source (data source)
func source(ctx context.Context, count int) <-chan DataPoint {
    out := make(chan DataPoint, 100)
    go func() {
        defer close(out)
        for i := 0; i < count; i++ {
            select {
            case <-ctx.Done():
                return
            case out <- DataPoint{
                ID:        i,
                Value:     rand.Float64() * 100,
                Timestamp: time.Now(),
            }:
            }
        }
    }()
    return out
}

// Stage 2: Normalize (Fan-out)
func normalize(ctx context.Context, in <-chan DataPoint, workers int) <-chan ProcessedData {
    out := make(chan ProcessedData, 100)
    var wg sync.WaitGroup

    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for dp := range orDoneDataPoint(ctx.Done(), in) {
                select {
                case <-ctx.Done():
                    return
                case out <- ProcessedData{
                    DataPoint:  dp,
                    Normalized: true,
                    Category:   categorize(dp.Value),
                }:
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

// Stage 3: Detect Anomaly (Fan-out)
func detectAnomaly(ctx context.Context, in <-chan ProcessedData, workers int) <-chan ProcessedData {
    out := make(chan ProcessedData, 100)
    var wg sync.WaitGroup

    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for pd := range orDoneProcessedData(ctx.Done(), in) {
                pd.Anomaly = pd.Value > 90 || pd.Value < 10
                select {
                case <-ctx.Done():
                    return
                case out <- pd:
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

// Stage 4: Sink (output, Fan-in)
func sink(ctx context.Context, in <-chan ProcessedData) {
    var normalCount, anomalyCount int
    for pd := range orDoneProcessedData(ctx.Done(), in) {
        if pd.Anomaly {
            anomalyCount++
            fmt.Printf("[ANOMALY] ID=%d, Value=%.2f, Category=%s\n",
                pd.ID, pd.Value, pd.Category)
        } else {
            normalCount++
        }
    }
    fmt.Printf("\nSummary: normal=%d, anomaly=%d\n", normalCount, anomalyCount)
}

// ---------- Helper functions ----------

func orDoneDataPoint(done <-chan struct{}, in <-chan DataPoint) <-chan DataPoint {
    out := make(chan DataPoint)
    go func() {
        defer close(out)
        for {
            select {
            case <-done:
                return
            case v, ok := <-in:
                if !ok {
                    return
                }
                select {
                case out <- v:
                case <-done:
                    return
                }
            }
        }
    }()
    return out
}

func orDoneProcessedData(done <-chan struct{}, in <-chan ProcessedData) <-chan ProcessedData {
    out := make(chan ProcessedData)
    go func() {
        defer close(out)
        for {
            select {
            case <-done:
                return
            case v, ok := <-in:
                if !ok {
                    return
                }
                select {
                case out <- v:
                case <-done:
                    return
                }
            }
        }
    }()
    return out
}

func categorize(value float64) string {
    switch {
    case value < 30:
        return "low"
    case value < 70:
        return "medium"
    default:
        return "high"
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    fmt.Println("Starting real-time data processing pipeline...")

    // Build Pipeline
    data := source(ctx, 1000)
    normalized := normalize(ctx, data, 5)           // 5 normalize workers
    detected := detectAnomaly(ctx, normalized, 3)   // 3 anomaly detection workers
    sink(ctx, detected)

    fmt.Println("Pipeline processing complete")
}
💡 Dica: Em um ambiente de produção, cada etapa do pipeline deve ter seu próprio tamanho de buffer, lógica de repetição de tentativas e métricas de monitoramento. Você pode usar expvar ou prometheus para exibir a velocidade de processamento, o comprimento da fila e a taxa de erros de cada etapa.


❓ Perguntas Frequentes

P: Qual é a diferença entre um Pipeline e um Worker Pool? R: Um Pipeline é um fluxo de dados que passa por várias etapas de processamento, sendo que cada etapa executa uma única ação (por exemplo, análise → transformação → salvamento), e a estrutura dos dados pode sofrer alterações. Um pool de trabalhadores consiste em um número fixo de trabalhadores que recuperam tarefas da mesma fila de trabalhos, e o formato dos dados permanece inalterado (por exemplo, compressão de imagem).

P: Quais são os conceitos fundamentais por trás do fan-out e do fan-in? R: O fan-out distribui dados de um único canal para vários workers para processamento paralelo (a fim de aumentar a taxa de processamento). O fan-in agrupa os resultados de vários canais em um único canal (para consumo unificado). Os dois são normalmente utilizados em conjunto.

P: Quando é necessário usar o Or-Done? R: Ao ler de um canal cujo ciclo de vida da goroutine você não pode controlar (como um canal retornado por uma biblioteca de terceiros). O Or-Done garante que a goroutine de leitura não fique bloqueada nas operações do canal caso o chamador cancele a operação. Para cenários simples, você pode usar select com um canal done.

P: Como faço para escolher entre errgroup e sync.WaitGroup? R: Use errgroup quando precisar coletar erros ou propagar cancelamentos. Use sync.WaitGroup (que é mais leve) quando precisar apenas aguardar a conclusão de uma goroutine. errgroup é um superconjunto de WaitGroup em termos de funcionalidade, mas introduz uma goroutine adicional para gerenciar cancelamentos.

P: Como se determina o tamanho do buffer para um canal em um pipeline? R: O princípio básico é garantir que os produtores não sejam bloqueados pelos consumidores. Um buffer muito grande desperdiça memória, enquanto um muito pequeno aumenta a troca de contexto das goroutines. Regra geral: tamanho do buffer = taxa de produção × latência desejada. Determine o tamanho real por meio de testes de carga em produção.

P: Como é possível evitar vazamentos de goroutines? R: Três regras: (1) Sempre que uma goroutine for criada, deve haver um mecanismo de saída (fechamento do canal ou um sinal done); (2) Use errgroup ou WaitGroup para gerenciar o ciclo de vida; (3) Use Or-Done para encapsular canais não controlados. Nunca crie uma goroutine se não souber quando ela será encerrada.

P: Esses padrões podem ser usados em combinação? R: Sim, e devem ser. Por exemplo, em cada etapa de um pipeline, você pode usar um conjunto de trabalhadores (Worker Pool) combinado com Fan-out/Fan-in. Um errgroup pode atuar como um orquestrador do pipeline para gerenciar centralmente cancelamentos e erros.


📖 Resumo


📝 Exercícios

  1. Problema básico (Dificuldade ⭐): Implemente um pipeline de processamento numérico de três etapas: gerar (1–100) → filtrar (manter apenas os números pares) → somar (acumular). Cada etapa é executada em uma goroutine separada e está conectada por meio de um canal.

  2. Problema avançado (Dificuldade ⭐⭐): Implemente um sistema de processamento simultâneo de logs: 10 workers de análise (fan-out) + 3 workers de gravação (fan-in) + tratamento de erros via errgroup. Simule 1.000 entradas de log e gere estatísticas (tempo total decorrido, número de entradas processadas, número de erros).

  3. Desafio (Dificuldade: ⭐⭐⭐): Projete e implemente um conjunto de workers com balanceamento de carga e escalonamento dinâmico. Requisitos: (1) Comece com 5 workers; (2) Aumente automaticamente o número de workers (até 20) quando o acúmulo da fila de tarefas exceder um limite; (3) Reduza o número de workers (até 2) quando a fila estiver vazia; (4) Use Context para controlar o encerramento dos workers; (5) Use -race para verificar se não há condição de corrida.

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%