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á
- Padrão Pipeline
- Fan-out / Fan-in
- Banco de candidatos
- Modo simplificado do canal Or-Done
- Propagação do cancelamento
- Grupo de erros
golang.org/x/sync/errgroup - Prática: Pipeline de processamento de dados em tempo real
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:
// 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
// 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
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)
}
graph LR
A[generate] -->|chan int| B[square]
B -->|chan int| C[double]
C -->|chan int| D[main]
<-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
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)
}
}
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
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)
}
}
(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”
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
}
7. Propagação de erros no errgroup
⚙️ Pré-requisito: Execute
go get golang.org/x/sync/errgroupantes de usar este pacote.
(1) ▶ Exemplo: errgroup
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
}
(2) ▶ Exemplo: errgroup + tempo limite de contexto
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)
}
}
(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
// 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")
}
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
selectcom um canaldone.
P: Como faço para escolher entre
errgroupesync.WaitGroup? R: Useerrgroupquando precisar coletar erros ou propagar cancelamentos. Usesync.WaitGroup(que é mais leve) quando precisar apenas aguardar a conclusão de uma goroutine.errgroupé um superconjunto deWaitGroupem 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) UseerrgroupouWaitGrouppara gerenciar o ciclo de vida; (3) UseOr-Donepara 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
- Pipeline: os dados passam por várias etapas de processamento, com uma goroutine por etapa
- Fan-out: Um único canal é distribuído entre vários trabalhadores (para processamento paralelo)
- Fan-in: Combinação de vários canais em um único canal (para consumo unificado)
- Conjunto de trabalhadores: um número fixo de trabalhadores retira tarefas da fila de trabalhos
- Or-Done: Consuma com segurança canais incontroláveis com suporte a cancelamento
- errgroup: Gerencia grupos de goroutines, oferecendo suporte à coleta de erros e à propagação de cancelamentos
- Modo combinado: o Pipeline utiliza internamente um conjunto de trabalhadores (Worker Pool) + Fan-out/Fan-in
📝 Exercícios
-
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.
-
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).
-
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
Contextpara controlar o encerramento dos workers; (5) Use-racepara verificar se não há condição de corrida.