Padrões de Concorrência em Go: 7 Padrões de Produção do Worker Pool ao Pipeline
Quando Vazamentos de Goroutine Encontram Concorrência Sem Limites: Um Pesadelo em Produção
3 da manhã, alerta de OOM em produção. A investigação revela: um handler HTTP lançando goroutines sem limites — 3 goroutines por requisição, QPS 1000 significa 3.000, e em 10 minutos, 180.000 goroutines vazadas consomem toda a memória. Pior ainda, essas goroutines mantêm referências de canais que bloqueiam o GC, eventualmente crashando o nó inteiro.
Este não é um caso isolado. As primitivas de concorrência do Go são simples — go func() lança concorrência em uma linha — mas programação de concorrência em produção vai muito além de apenas iniciar goroutines. Você precisa controlar níveis de concorrência, lidar com propagação de erros, implementar desligamento graceful e prevenir vazamentos de recursos. Este artigo cobre 7 padrões de concorrência de nível produtivo para ajudá-lo a construir serviços concorrentes robustos em Go.
Referência de Conceitos Centrais
| Primitiva | Propósito | Características Principais | Caso de Uso Típico |
|---|---|---|---|
goroutine |
Unidade de execução concorrente leve | Stack crescente (2KB inicial), agendamento pelo runtime do Go | Qualquer tarefa que requer execução concorrente |
channel |
Comunicação entre goroutines | Type-safe, com/sem buffer, fechável | Passagem de dados, notificação de sinais, coleta de resultados |
sync.WaitGroup |
Esperar por um grupo de goroutines | Tríade Add/Done/Wait |
Espera de tarefas em lote, fan-out concorrente |
sync.Mutex |
Exclusão mútua para estado compartilhado | Usável com valor zero, suporte a TryLock (Go 1.18+) |
Contadores, atualizações de cache, hot-reload de config |
context.Context |
Propagar cancelamento e timeout | Imutável, só pode derivar contextos filhos | Timeout de requisição, desligamento graceful, tracing |
errgroup.Group |
Execução concorrente + coleta de erros | Primeiro erro cancela todas as goroutines | Chamadas de API em lote, busca paralela de dados |
semaphore.Weighted |
Limitação de taxa com semáforo ponderado | Suporta pesos, aquisição com timeout | Limitação de API, controle de cota de recursos |
5 Desafios da Programação de Concorrência em Produção
Desafio 1: Concorrência Sem Limites Causa Esgotamento de Recursos
func handleRequests(urls []string) {
for _, url := range urls {
go fetch(url)
}
}
Cada URL cria uma goroutine — 10.000 URLs significa 10.000 conexões concorrentes. Pools de conexões com banco de dados são esgotados, serviços downstream são sobrecarregados, a memória dispara.
Desafio 2: Vazamentos de Goroutine
func process(ch <-chan int) {
for {
val := <-ch
fmt.Println(val)
}
}
Se o channel nunca for fechado, esta goroutine nunca sai. Em serviços de longa duração, goroutines vazadas se acumulam continuamente.
Desafio 3: Erros Silenciosamente Ignorados
func fetchAll(urls []string) {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
log.Printf("fetch %s failed: %v", u, err)
return
}
process(resp)
}(url)
}
wg.Wait()
}
Erros são apenas logados — o chamador não tem ideia das falhas. Se 2 de 3 URLs falharem, o chamador assume que todas tiveram sucesso.
Desafio 4: Sem Desligamento Graceful
Quando o serviço recebe SIGTERM, as goroutines em execução são interrompidas à força. Escritas em andamento podem corromper dados, transações em progresso podem ficar incompletas.
Desafio 5: Dificuldade em Compor Padrões de Concorrência
Worker Pool precisa de limitação de taxa, Pipeline precisa de propagação de erros, Fan-out precisa de agregação de resultados. Implementar cada padrão isoladamente não é difícil, mas combiná-los em um serviço cria interações que facilmente levam a deadlocks.
7 Padrões de Concorrência de Nível Produtivo
Padrão 1: Worker Pool — Pool de Goroutines Limitado
Worker Pool é o padrão de concorrência mais fundamental e importante. Ideia central: um número fixo de workers retira tarefas de uma fila de tarefas, evitando concorrência sem limites.
package workerpool
import (
"context"
"sync"
)
type Task func(ctx context.Context) error
type Pool struct {
workers int
tasks chan Task
wg sync.WaitGroup
}
func NewPool(workers int, bufferSize int) *Pool {
return &Pool{
workers: workers,
tasks: make(chan Task, bufferSize),
}
}
func (p *Pool) Start(ctx context.Context) {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go func(workerID int) {
defer p.wg.Done()
for {
select {
case <-ctx.Done():
return
case task, ok := <-p.tasks:
if !ok {
return
}
_ = task(ctx)
}
}
}(i)
}
}
func (p *Pool) Submit(task Task) bool {
select {
case p.tasks <- task:
return true
default:
return false
}
}
func (p *Pool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Exemplo de uso:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
pool := NewPool(10, 100)
pool.Start(ctx)
urls := []string{
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/products",
}
for _, url := range urls {
u := url
pool.Submit(func(ctx context.Context) error {
return fetchURL(ctx, u)
})
}
pool.Stop()
}
Pontos-chave de design:
- Contagem fixa de workers previne explosão de goroutines
- Channel de tarefas com buffer serve como fila de tarefas
- Suporte a Context para cancelamento, workers podem sair gracefulmente
Submité non-blocking, retorna false quando a fila está cheia
Padrão 2: Fan-out/Fan-in — Dispersão-Coleta Paralela
Fan-out distribui uma fonte de dados para múltiplas goroutines para processamento paralelo, Fan-in mescla resultados de múltiplas goroutines em um único channel.
package fan
import (
"context"
"sync"
)
func FanOut[T any](ctx context.Context, source <-chan T, workers int) []<-chan T {
channels := make([]<-chan T, workers)
for i := 0; i < workers; i++ {
ch := make(chan T)
channels[i] = ch
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
case val, ok := <-source:
if !ok {
return
}
select {
case ch <- val:
case <-ctx.Done():
return
}
}
}
}()
}
return channels
}
func FanIn[T any](ctx context.Context, channels ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan T) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case val, ok := <-c:
if !ok {
return
}
select {
case out <- val:
case <-ctx.Done():
return
}
}
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Exemplo de uso — processamento paralelo de pedidos:
func processOrders(ctx context.Context, orders <-chan Order) <-chan Result {
workers := FanOut(ctx, orders, 5)
return FanIn(ctx, workers...)
}
Pontos-chave de design:
- Suporte a genéricos (Go 1.18+), aplicável a qualquer tipo
- Cada goroutine de fan-out consome independentemente do channel fonte
- Fan-in usa WaitGroup para esperar que todos os channels de entrada fechem
- Todas as goroutines podem sair no cancelamento do contexto
Padrão 3: Pipeline — Processamento em Estágios
Pipeline decompõe processamento complexo em estágios, cada um executando como uma goroutine conectada por channels.
package pipeline
import (
"context"
)
type Stage[In any, Out any] func(ctx context.Context, in <-chan In) <-chan Out
func NewPipeline[In any, Out any](
ctx context.Context,
source <-chan In,
stages ...Stage[In, In],
) <-chan Out {
current := source
for _, stage := range stages {
current = stage(ctx, current)
}
return any(current).(<-chan Out)
}
func NewStage[In any, Out any](
process func(ctx context.Context, in In) (Out, error),
bufferSize int,
) Stage[In, Out] {
return func(ctx context.Context, in <-chan In) <-chan Out {
out := make(chan Out, bufferSize)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case val, ok := <-in:
if !ok {
return
}
result, err := process(ctx, val)
if err != nil {
continue
}
select {
case out <- result:
case <-ctx.Done():
return
}
}
}
}()
return out
}
}
Exemplo de uso — pipeline de processamento de dados:
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
raw := make(chan RawData, 100)
validate := NewStage[RawData, ValidData](func(ctx context.Context, in RawData) (ValidData, error) {
if err := in.Validate(); err != nil {
return ValidData{}, err
}
return in.ToValid(), nil
}, 50)
enrich := NewStage[ValidData, EnrichedData](func(ctx context.Context, in ValidData) (EnrichedData, error) {
return fetchExtraInfo(ctx, in)
}, 50)
transform := NewStage[EnrichedData, FinalData](func(ctx context.Context, in EnrichedData) (FinalData, error) {
return in.Transform()
}, 50)
result := NewPipeline(ctx, raw, validate, enrich, transform)
go func() {
for r := range result {
saveToDB(r)
}
}()
}
Pontos-chave de design:
- Cada estágio é uma goroutine independente, pode escalar independentemente
- Channels fornecem backpressure entre estágios
- Erros tratados dentro dos estágios, não interrompem o pipeline inteiro
- Todos os estágios saem gracefulmente no cancelamento do contexto
Padrão 4: errgroup — Tratamento de Erros Concorrentes
errgroup é a versão com consciência de erros do sync.WaitGroup: o primeiro erro cancela todas as goroutines.
package main
import (
"context"
"fmt"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
type FetchResult struct {
URL string
Body string
Size int
}
func fetchMultiple(ctx context.Context, urls []string) ([]FetchResult, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]FetchResult, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("create request %s: %w", url, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("fetch %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read %s: %w", url, err)
}
results[i] = FetchResult{
URL: url,
Body: string(body),
Size: len(body),
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
errgroup com limite de concorrência:
func fetchWithLimit(ctx context.Context, urls []string, maxConcurrent int) ([]FetchResult, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(maxConcurrent)
results := make([]FetchResult, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
results[i], _ = fetchOne(ctx, url)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Pontos-chave de design:
errgroup.WithContextpropaga automaticamente o cancelamentog.SetLimit(n)controla a concorrência máxima (Go 1.20+)- O primeiro erro cancela todas as goroutines em andamento
- Captura de variável em closure requer
i, url := i, url
Padrão 5: Semáforo — Limitação de Taxa
semaphore.Weighted fornece semáforos ponderados para alocação de recursos com diferentes pesos.
package ratelimit
import (
"context"
"fmt"
"golang.org/x/sync/semaphore"
)
type RateLimiter struct {
sem *semaphore.Weighted
}
func NewRateLimiter(maxWeight int64) *RateLimiter {
return &RateLimiter{
sem: semaphore.NewWeighted(maxWeight),
}
}
func (r *RateLimiter) Do(ctx context.Context, weight int64, fn func() error) error {
if err := r.sem.Acquire(ctx, weight); err != nil {
return fmt.Errorf("acquire semaphore: %w", err)
}
defer r.sem.Release(weight)
return fn()
}
Exemplo de uso — limitação de taxa de API em camadas:
func main() {
limiter := NewRateLimiter(100)
err := limiter.Do(context.Background(), 10, func() error {
return callLightAPI()
})
err = limiter.Do(context.Background(), 50, func() error {
return callHeavyAPI()
})
}
Semáforo com timeout:
func (r *RateLimiter) DoWithTimeout(ctx context.Context, weight int64, timeout time.Duration, fn func() error) error {
acquireCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
if err := r.sem.Acquire(acquireCtx, weight); err != nil {
return fmt.Errorf("semaphore acquire timeout: %w", err)
}
defer r.sem.Release(weight)
return fn()
}
Pontos-chave de design:
- Semáforo ponderado, operações diferentes consomem cotas diferentes
Acquiresuporta cancelamento de contexto e timeoutReleasedeve ser pareado comAcquire- Adequado para limitação de API em camadas, controle de cota de recursos
Padrão 6: Cancelamento e Timeout de Contexto
Context é a "linha da vida" da programação de concorrência em Go, usado para propagar sinais de cancelamento, timeouts e deadlines.
package ctxutil
import (
"context"
"fmt"
"time"
)
type Result struct {
Data string
Error error
}
func FetchWithTimeout(ctx context.Context, url string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("fetch %s timed out after %v: %w", url, timeout, ctx.Err())
}
return "", fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read response: %w", err)
}
return string(body), nil
}
func BatchFetch(ctx context.Context, urls []string, timeout time.Duration) []Result {
results := make([]Result, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
data, err := FetchWithTimeout(ctx, u, timeout)
results[idx] = Result{Data: data, Error: err}
}(i, url)
}
wg.Wait()
return results
}
Padrão de desligamento graceful:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
server := &http.Server{Addr: ":8080"}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
_ = server.ListenAndServe()
}
Pontos-chave de design:
WithTimeout/WithDeadlinepara definir timeoutsWithCancelpara controle manual de cancelamentosignal.NotifyContextpara escutar sinais do sistema- O cancelamento do contexto propaga para todas as goroutines filhas
defer cancel()previne vazamentos de contexto
Padrão 7: Serviço Concorrente em Produção — Combinando Padrões
Combine os padrões acima para construir um serviço concorrente de nível produtivo: Worker Pool + Pipeline + errgroup + Context.
package concurrencyservice
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
type Service struct {
workers int
bufferSize int
timeout time.Duration
}
func NewService(workers, bufferSize int, timeout time.Duration) *Service {
return &Service{
workers: workers,
bufferSize: bufferSize,
timeout: timeout,
}
}
type Job struct {
ID string
Input any
}
type Output struct {
Job Job
Result any
Err error
}
func (s *Service) Process(ctx context.Context, jobs []Job) ([]Output, error) {
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(s.workers)
jobCh := make(chan Job, s.bufferSize)
resultCh := make(chan Output, s.bufferSize)
g.Go(func() error {
defer close(jobCh)
for _, job := range jobs {
select {
case <-ctx.Done():
return ctx.Err()
case jobCh <- job:
}
}
return nil
})
var processWg sync.WaitGroup
for i := 0; i < s.workers; i++ {
processWg.Add(1)
go func() {
defer processWg.Done()
for job := range jobCh {
output := Output{Job: job}
output.Result, output.Err = s.processOne(ctx, job)
select {
case resultCh <- output:
case <-ctx.Done():
return
}
}
}()
}
go func() {
processWg.Wait()
close(resultCh)
}()
var outputs []Output
for result := range resultCh {
outputs = append(outputs, result)
}
if err := g.Wait(); err != nil {
return outputs, fmt.Errorf("service process: %w", err)
}
return outputs, nil
}
func (s *Service) processOne(ctx context.Context, job Job) (any, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return fmt.Sprintf("processed-%s", job.ID), nil
}
Pontos-chave de design:
- errgroup controla concorrência + propagação de erros
- Channels para distribuição de tarefas e coleta de resultados
- Context para timeout e cancelamento unificados
- WaitGroup garante que todos os workers completem antes de fechar o channel de resultados
- Design em camadas: agendamento (errgroup) + execução (worker goroutines) + coleta (range resultCh)
5 Armadilhas Comuns e Correções
Armadilha 1: Captura de Variável em Closure
❌ Errado:
for _, url := range urls {
go func() {
fetch(url)
}()
}
Todas as goroutines compartilham a mesma variável url, buscando no final a última URL.
✅ Correto:
for _, url := range urls {
url := url
go func() {
fetch(url)
}()
}
Armadilha 2: Channel Não Fechado Causa Vazamento de Goroutine
❌ Errado:
func producer() <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < 100; i++ {
ch <- i
}
}()
return ch
}
Consumidores iterando sobre o channel nunca terminam, goroutine vaza.
✅ Correto:
func producer() <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 100; i++ {
ch <- i
}
}()
return ch
}
Armadilha 3: Posição Incorreta do WaitGroup Add
❌ Errado:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(1)
defer wg.Done()
doWork()
}()
}
wg.Wait()
As goroutines podem ainda não ter alcançado wg.Add(1), e o wg.Wait() da goroutine principal passa imediatamente.
✅ Correto:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()
Armadilha 4: Vazamento do time.After no Select
❌ Errado:
select {
case result := <-ch:
process(result)
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
Cada select cria um novo channel time.After. Chamadas frequentes vazam timers.
✅ Correto:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case result := <-ch:
process(result)
case <-ctx.Done():
return ctx.Err()
}
Armadilha 5: Cópia de Mutex
❌ Errado:
type SafeMap struct {
mu sync.Mutex
data map[string]string
}
func copyMap(m SafeMap) SafeMap {
return m
}
O valor zero do Mutex é "desbloqueado". Copiar um Mutex bloqueado causa deadlock ou data race.
✅ Correto:
type SafeMap struct {
mu *sync.Mutex
data map[string]string
}
func NewSafeMap() *SafeMap {
return &SafeMap{
mu: &sync.Mutex{},
data: make(map[string]string),
}
}
Referência de Diagnóstico de Erros
| Sintoma | Possível Causa | Investigação | Solução |
|---|---|---|---|
fatal error: all goroutines are asleep - deadlock! |
Todas as goroutines bloqueadas, sem goroutines ativas | Verifique pareamento de leitura/escrita em channels, select default | Garanta que channels tenham produtores e consumidores, adicione cancelamento de contexto |
| Contagem de goroutines cresce continuamente | Vazamento de goroutines, channels não fechados | Monitoramento com runtime.NumGoroutine(), pprof goroutine profile |
Garanta que todas as goroutines tenham caminhos de saída, defer close(ch) |
| Memória cresce continuamente | Goroutines mantêm referências a objetos grandes, acúmulo em channels | pprof heap profile, verifique tamanho do buffer do channel | Limite o buffer do channel, libere referências prontamente |
Erros frequentes de context canceled |
Contexto upstream cancelado, timeout muito curto | Verifique a cadeia de contextos, confirme se o timeout é razoável | Ajuste o timeout, distinga erros de negócio de erros de timeout |
| Resultados concorrentes faltando | Goroutine sai antes de escrever no channel | Verifique lógica de saída da goroutine, confirme fechamento do channel | Use WaitGroup para garantir que todas as goroutines completem antes de fechar o channel |
race condition detectada |
Múltiplas goroutines leem/escrevem variáveis compartilhadas | go test -race, verifique variáveis globais e capturas em closures |
Use Mutex/RWMutex para proteção, ou mude para comunicação via channel |
| Channel bloqueado, sem resposta | Descompasso de velocidade entre produtor/consumidor | Verifique tamanho do buffer do channel, monitore taxas de produção/consumo | Aumente o buffer, use select+default para envio non-blocking |
| Falha no desligamento graceful | Goroutines não respondem ao cancelamento de contexto | Verifique se as goroutines fazem select em ctx.Done() | Garanta que todas as goroutines de longa duração verifiquem ctx.Done() |
| errgroup retorna apenas um erro | errgroup foi projetado para cancelamento no primeiro erro | Verifique se você precisa coletar todos os erros | Use um coletor de erros personalizado ou múltiplos errgroups |
| Timeout na aquisição do semáforo | Limite de concorrência muito restrito, requisições enfileiradas por muito tempo | Monitore tempo de espera do semáforo, ajuste cotas de peso | Aumente a capacidade do semáforo, otimize a alocação de pesos |
Técnicas Avançadas de Otimização
Otimização 1: Worker Pool Dinâmico
Ajuste dinamicamente a contagem de workers com base na carga do sistema:
package dynamicpool
import (
"context"
"runtime"
"sync"
"sync/atomic"
"time"
)
type DynamicPool struct {
minWorkers int64
maxWorkers int64
active atomic.Int64
tasks chan Task
wg sync.WaitGroup
adjustTick *time.Ticker
}
func NewDynamicPool(minW, maxW int) *DynamicPool {
return &DynamicPool{
minWorkers: int64(minW),
maxWorkers: int64(maxW),
tasks: make(chan Task, maxW*2),
adjustTick: time.NewTicker(5 * time.Second),
}
}
func (p *DynamicPool) Start(ctx context.Context) {
for i := 0; i < int(p.minWorkers); i++ {
p.addWorker(ctx)
}
go func() {
for {
select {
case <-ctx.Done():
p.adjustTick.Stop()
return
case <-p.adjustTick.C:
p.adjustWorkers(ctx)
}
}
}()
}
func (p *DynamicPool) addWorker(ctx context.Context) {
p.active.Add(1)
p.wg.Add(1)
go func() {
defer func() {
p.active.Add(-1)
p.wg.Done()
}()
for task := range p.tasks {
_ = task(ctx)
}
}()
}
func (p *DynamicPool) adjustWorkers(ctx context.Context) {
current := p.active.Load()
queueLen := len(p.tasks)
if queueLen > int(current) && current < p.maxWorkers {
p.addWorker(ctx)
} else if queueLen == 0 && current > p.minWorkers {
for i := 0; i < 5 && int(p.active.Load()) > int(p.minWorkers); i++ {
p.tasks <- nil
}
}
}
func (p *DynamicPool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Otimização 2: Batching
Combine múltiplas tarefas pequenas em uma operação em lote para reduzir overhead de syscall e IO:
package batcher
import (
"context"
"sync"
"time"
)
type Batcher[T any, R any] struct {
batchSize int
flushInterval time.Duration
handler func(ctx context.Context, batch []T) ([]R, error)
mu sync.Mutex
buffer []T
results []R
}
func NewBatcher[T any, R any](
batchSize int,
flushInterval time.Duration,
handler func(ctx context.Context, batch []T) ([]R, error),
) *Batcher[T, R] {
return &Batcher[T, R]{
batchSize: batchSize,
flushInterval: flushInterval,
handler: handler,
buffer: make([]T, 0, batchSize),
}
}
func (b *Batcher[T, R]) Add(ctx context.Context, item T) (R, error) {
b.mu.Lock()
b.buffer = append(b.buffer, item)
if len(b.buffer) >= b.batchSize {
batch := b.buffer
b.buffer = make([]T, 0, b.batchSize)
b.mu.Unlock()
results, err := b.handler(ctx, batch)
if err != nil {
var zero R
return zero, err
}
b.results = append(b.results, results...)
var zero R
if len(results) > 0 {
return results[0], nil
}
return zero, nil
}
b.mu.Unlock()
var zero R
return zero, nil
}
func (b *Batcher[T, R]) Flush(ctx context.Context) ([]R, error) {
b.mu.Lock()
batch := b.buffer
b.buffer = make([]T, 0, b.batchSize)
b.mu.Unlock()
if len(batch) == 0 {
return b.results, nil
}
results, err := b.handler(ctx, batch)
if err != nil {
return nil, err
}
b.results = append(b.results, results...)
return b.results, nil
}
Otimização 3: Passagem por Channel Zero-Copy
Use ponteiros e sync.Pool para reduzir alocação de memória durante passagem por channels:
package zerocopy
import (
"sync"
)
type Buffer struct {
Data []byte
}
var bufferPool = sync.Pool{
New: func() any {
return &Buffer{Data: make([]byte, 0, 4096)}
},
}
func GetBuffer() *Buffer {
return bufferPool.Get().(*Buffer)
}
func PutBuffer(buf *Buffer) {
buf.Data = buf.Data[:0]
bufferPool.Put(buf)
}
func ProcessPipeline(ctx context.Context, input <-chan []byte) <-chan *Buffer {
out := make(chan *Buffer, 64)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case data, ok := <-input:
if !ok {
return
}
buf := GetBuffer()
buf.Data = append(buf.Data[:0], data...)
select {
case out <- buf:
case <-ctx.Done():
PutBuffer(buf)
return
}
}
}
}()
return out
}
Comparação de Padrões de Concorrência
| Característica | goroutine+channel | sync.WaitGroup | errgroup | Worker Pool |
|---|---|---|---|---|
| Controle de concorrência | Sem controle embutido | Sem controle embutido | SetLimit |
Contagem fixa de workers |
| Tratamento de erros | Implementação manual | Implementação manual | Cancelamento automático no primeiro erro | Implementação manual |
| Backpressure | Buffer do channel | Nenhum | Nenhum | Channel de tarefas |
| Propagação de cancelamento | Context manual | Context manual | Context automático | Context manual |
| Coleta de resultados | Recebimento por channel | Variáveis compartilhadas | Retorna slice | Recebimento por channel |
| Caso de uso | Processamento de dados em stream | Espera de tarefas em lote | Chamadas de API concorrentes | Processamento de tarefas com limitação de taxa |
| Complexidade | Média | Baixa | Baixa | Média |
| Contagem de goroutines | Variável | Variável | Limitada | Fixa |
| Recomendação para produção | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Resumo
Programação de concorrência em Go não é sobre "apenas fazer rodar" — é sobre responder quatro perguntas: Quantas goroutines estão em execução? Quando elas saem? O que acontece em caso de erro? Como os recursos são recuperados? Worker Pool responde "quantas", Context responde "quando sair", errgroup responde "o que em caso de erro", e defer close() responde "como recuperar". Domine estes 7 padrões, e você terá a metodologia central para programação de concorrência em Go de nível produtivo.
Ferramentas Recomendadas
- Formatador JSON — Formate respostas JSON de serviços concorrentes, depure rapidamente problemas de estrutura de dados
- Calculadora de Hash — Compute assinaturas de requisição e checksums de dados, garanta consistência de dados em requisições concorrentes
- Codificação/Decodificação Base64 — Trate codificação de dados binários em serviços concorrentes
Experimente estas ferramentas executadas localmente no navegador — nenhum cadastro necessário →