---
title: "Channels em Go: Comunicação entre Goroutines para Produção"
url: "https://golang.com.br/blog/channels-go-comunicacao-goroutines-producao/"
markdown_url: "https://golang.com.br/blog/channels-go-comunicacao-goroutines-producao.MD"
description: "Domine channels em Go em serviços reais: semântica de send/receive, select, canais bufferizados, nil channels, fechamento seguro, fan-in/fan-out, pipelines, backpressure e prevenção de vazamento de goroutines. Guia PT-BR com exemplos de produção."
date: "2026-06-21"
author: "Golang Brasil"
---

# Channels em Go: Comunicação entre Goroutines para Produção

Domine channels em Go em serviços reais: semântica de send/receive, select, canais bufferizados, nil channels, fechamento seguro, fan-in/fan-out, pipelines, backpressure e prevenção de vazamento de goroutines. Guia PT-BR com exemplos de produção.


A premissa central de Go é "não se comunique compartilhando memória; compartilhe memória se comunicando". Channels são a materialização dessa ideia: filas tipadas, seguras para concorrência, que conectam goroutines sem que você precise escrever um único `mutex`. O problema é que a interface simples (`ch <- v`, `v := <-ch`) esconde regras de fechamento, direção, bloqueio e vazamento que decidem se o serviço aguenta pico ou se corro de madrugada atrás de goroutine leak. Este guia cobre o uso de channels em produção, complementando o [guia de concorrência com goroutines e channels](/aprenda/concorrencia-go/), os [padrões de concorrência (worker pool, fan-out/fan-in)](/tutoriais/go-concurrency-patterns/), o [guia de errgroup para fan-out com erro e cancelamento](/blog/errgroup-go-concorrencia-cancelamento-erros/) e o [guia de context para timeout e cancelamento](/blog/context-timeout-cancelamento-go/).

## O modelo mental: send, receive, close

Um channel é uma fila FIFO concorrente. Cada operação tem um comportamento bem definido que você precisa saber de cor antes de escrever um serviço:

| Operação | Estado do canal | Resultado |
| --- | --- | --- |
| `ch <- v` | sem buffer, sem receptor | bloqueia até alguém receber |
| `ch <- v` | buffer cheio | bloqueia até alguém receber |
| `ch <- v` | buffer com espaço | não bloqueia |
| `v := <-ch` | vazio, aberto | bloqueia até alguém enviar |
| `v := <-ch` | vazio, fechado | retorna zero-value, `ok` false |
| `ch <- v` | fechado | **panic** |
| `close(ch)` | já fechado | **panic** |

As duas linhas de **panic** são a fonte da maioria dos bugs reais. Quem fecha um canal é o **remetente**, nunca o receptor — porque só quem sabe que não vai mais enviar pode fechar com segurança. Receptores devem usar `range` ou a forma `v, ok := <-ch` para detectar o fechamento.

```go
func producer(out chan<- int) {
    defer close(out)
    for i := 0; i < 10; i++ {
        out <- i
    }
}

func consumer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
}
```

O `defer close(out)` no remetente é o padrão correto. Fechar do lado do receptor é a forma mais comum de introduzir um panic concorrente em produção.

## Canais bufferizados: não são uma fila de mensagens

O tamanho do buffer é uma decisão de projeto, não um ajuste fino. Três tamanhos cobrem quase todos os casos em produção:

- **0 (sem buffer):** handoff síncrono. Cada send bloqueia até um receptor estar pronto. É o padrão para sinalização e para garantir backpressure natural entre produtor e consumidor.
- **1:** usado em padrões como "result ou done" (`chan struct{}`), onde o buffer absorve exatamente um envio tardio depois do receptor já ter desistido.
- **N (limitado):** usado para suavizar rajadas, normalmente pareado com `len(ch)` em health checks ou com `select` + `default` para descarte sob pressão.

O erro clássico é achar que um buffer gigante (`make(chan int, 100000)`) resolve problemas de lentidão no consumidor. Ele só adia: o produtor continua na mesma vazão até o buffer encher, e então bloqueia da mesma forma. O buffer esconde o problema durante o pico e explode quando a memória acaba. Se o consumidor é mais lento que o produtor de forma persistente, a solução é mais consumidores, `context.WithTimeout` no send, ou descarte explícito — nunca um buffer maior.

```go
// backpressure explícito com timeout: preferível a buffer gigante
select {
case out <- job:
case <-ctx.Done():
    return ctx.Err()
}
```

## select: a ferramenta mais importante de concorrência

`select` escolhe exatamente uma operação de canal pronta, aleatoriamente se várias estiverem prontas. Ele é o `switch` da concorrência e a base de todos os padrões de produção:

```go
select {
case v := <-src:
    process(v)
case <-ctx.Done():
    return ctx.Err()
case <-time.After(5 * time.Second):
    return errTimeout
}
```

Três formas merecem decorar:

1. **`case` + `default`:** torna o `select` não bloqueante. Útil para heartbeat, descarte de mensagens sob pressão e detecção de canal cheio.
2. **`case <-ctx.Done()`:** garante que todo `select` que espera em um canal de trabalho também respeita cancelamento. Sempre inclua quando o canal puder bloquear indefinidamente.
3. **`case <-time.After(d)`:** timeout por operação. Cuidado: `time.After` aloca um timer que só é coletado depois de disparar, então em loops de alta frequência prefira `time.NewTimer` com `Reset`, ou um único `time.Ticker`.

`select` com todos os cases bloqueando e sem `default` suspende a goroutine até qualquer um ficar pronto. É exatamente o que torna pipelines elegantes em Go.

## Pipelines: estágios conectados por canais

Um pipeline é uma cadeia de estágios, cada um uma goroutine que lê de um canal, transforma e escreve em outro. O padrão escala bem porque cada estágio roda em paralelo e o backpressure flui naturalmente pelo tamanho dos buffers.

```go
func source(ctx context.Context, urls []string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for _, u := range urls {
            select {
            case out <- u:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

func fetch(ctx context.Context, in <-chan string) <-chan []byte {
    out := make(chan []byte, 8)
    go func() {
        defer close(out)
        for u := range in {
            body, err := httpGet(ctx, u)
            if err != nil {
                continue
            }
            select {
            case out <- body:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}
```

Cada estágio fecha seu canal de saída com `defer close(out)` e respeita `ctx.Done()` em cada send. Esse par de regras evita dois defeitos comuns: vazar a goroutine quando o consumidor desiste, e bloquear indefinidamente quando o contexto é cancelado.

Para distribuir trabalho entre N workers lendo do mesmo canal de entrada, o jeito idiomático é disparar N goroutines que compartilham o mesmo `in <-chan`, e coletar os resultados em um único `out` protegido por um `sync.WaitGroup` que fecha `out` quando todos terminam. Esse é exatamente o padrão de [worker pool com fila de jobs](/blog/worker-pool-go-fila-jobs/) e o que o [errgroup com SetLimit](/blog/errgroup-go-concorrencia-cancelamento-erros/) automatiza quando você precisa propagar erro e cancelar irmãs.

## nil channels como válvula seletora

Um `select` em um canal `nil` nunca é selecionado para aquela direção. Isso vira uma técnica elegante para desativar dinamicamente um case dentro de um loop:

```go
var in <-chan int = src
var out chan<- int = dst
for in != nil || out != nil {
    select {
    case v, ok := <-in:
        if !ok {
            in = nil // desativa este case
            continue
        }
        // prepara próximo envio
        pending = v
    case out <- pending:
        if pending == last {
            out = nil // desativa quando terminar
        }
    }
}
```

Esse padrão aparece em pipelines com fim bem definido (EOF) e em multiplexadores que combinam vários canais em um só. Vale conhecer porque é idiomático em código da biblioteca padrão e em bibliotecas como `golang.org/x/sync/errgroup` internamente.

## Fan-in: combinando vários canais em um

Fan-in é multiplexar N canais de entrada em um único canal de saída. A forma correta usa uma goroutine por fonte e um `sync.WaitGroup` para fechar a saída exatamente uma vez:

```go
func merge[T any](ctx context.Context, sources ...<-chan T) <-chan T {
    out := make(chan T)
    var wg sync.WaitGroup
    wg.Add(len(sources))
    for _, src := range sources {
        go func(s <-chan T) {
            defer wg.Done()
            for v := range s {
                select {
                case out <- v:
                case <-ctx.Done():
                    return
                }
            }
        }(src)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}
```

Duas armadilhas clássicas: (1) fechar `out` antes de todas as goroutines terminarem provoca panic no send; (2) esquecer `ctx.Done()` no send interno vaza a goroutine se o consumidor desistir antes de todas as fontes esgotarem. A versão acima resolve os dois. Em pipelines que precisam propagar o primeiro erro e cancelar irmãs, prefira [errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/) em vez de remontar essa lógica manualmente.

## Prevenção de vazamento de goroutines

Uma goroutine vaza quando ela bloqueia para sempre em um send ou receive que ninguém mais vai completar. Os dois sintomas clássicos em produção:

- Memória cresce lentamente e o perfil mostra milhares de goroutines em `chan send` ou `chan receive`.
- Latência p95 degrada ao longo do tempo até reiniciar o processo.

A regra fixa: **toda goroutine que bloqueia em canal precisa de um caminho de saída**. Em geral isso é `ctx.Done()`. A auditoria consiste em, para cada `go func()`, confirmar que existe um `case <-ctx.Done()` (ou um `close` que a desbloqueie) em cada ponto de bloqueio.

```go
// pronto para produção: respeita cancelamento
go func() {
    defer wg.Done()
    for {
        select {
        case v, ok := <-in:
            if !ok {
                return
            }
            handle(v)
        case <-ctx.Done():
            return
        }
    }
}()
```

Um teste rápido para capturar vazamento é usar `goleak.VerifyNone(t)` no fim de cada teste que dispara goroutines. Ele falha o teste se alguma goroutine criada pela suíte ainda estiver viva, expondo vazamentos antes do deploy. Em serviços de longa duração, vale também exportar `runtime.NumGoroutine()` como métrica e alertar sobre crescimento monotônico.

## Sinalização com chan struct{}

O canal vazio `chan struct{}` é o idiomático para sinalização (onde o valor não importa, só o fato de ter sido enviado). Padrões comuns:

- **Done channel:** `done := make(chan struct{})` fechado para sinalizar fim; receptores fazem `<-done`.
- **Limit/semáforo:** `sem := make(chan struct{}, N)`; `sem <- struct{}{}` antes de rodar e `<-sem` ao terminar limita a N trabalhos simultâneos. Equivalente a um semáforo, sem importar bibliotecas.
- **One-shot broadcast:** fechar um `chan struct{}` libera todos os receptores ao mesmo tempo, porque receive em canal fechado retorna imediatamente.

```go
type Worker struct {
    stop chan struct{}
}

func (w *Worker) Run() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            w.tick()
        case <-w.stop:
            return
        }
    }
}

func (w *Worker) Stop() {
    close(w.stop)
}
```

Esse padrão de "stop channel" é a base de servidores HTTP, workers de fila e qualquer componente que precise desligar limpo. Combine com [graceful shutdown](/blog/graceful-shutdown-go-producao/) para fechar todos os canais na ordem certa durante o término do processo.

## Health check e observabilidade de canais

Em produção, canais saturados aparecem como latência e não como erro. Vale expor dois sinais:

1. **`len(ch)` / `cap(ch)`** exportados como gauge por canal crítico, para detectar fila crescendo antes de virar timeout.
2. **Contador de descarte** quando você usa `select` + `default` para descartar sob pressão, para medir quanto trabalho foi perdido.

Não exporte `len` de canais não bufferizados (sempre 0 ou 1, ruído) nem de canais que mudam de tamanho raramente. Concentre observabilidade nos pontos onde o buffer é estratégico — fila de jobs, fila de escrita em batch, fila de eventos para o outbox.

Para rastrear fluxo end-to-end, propague `traceID` pelo payload do canal (e não só pelo `context`), porque quando a mensagem sai da goroutine atual o `context` original pode ser substituído. O [guia de slog para logging estruturado](/blog/slog-go-logging-estruturado/) e o de [OpenTelemetry em Go](/blog/go-opentelemetry-observabilidade-tracing-metricas/) mostram como carregar esses campos em produção.

## Erros comuns em revisão de código

Uma lista prática do que procurar quando revisar código que usa channels:

- **Fechar canal do lado do receptor.** Quase sempre panic em algum caminho concorrente. Mova o `close` para o remetente.
- **`close` em canal compartilhado por vários remetentes.** Só um pode fechar; use um `sync.Once` ou um coordenador.
- **Esquecer `ctx.Done()` no `select`.** Vira goroutine leak no primeiro cancelamento.
- **Buffer gigante para "resolver" lentidão.** Troque por mais consumidores ou por descarte explícito.
- **`time.After` dentro de loop apertado.** Vira alocação de timer por iteração; use `time.NewTimer` com `Reset`.
- **Receber de canal fechado e tratar zero-value como dado.** Sempre use `v, ok := <-ch`.
- **Assumir ordem entre múltiplos cases prontos.** `select` é aleatório; se a ordem importa, separe em `select`s encadeados.

## Quando NÃO usar channels

Channels são elegantes, mas nem todo problema de concorrência é comunicação. Use mutex quando:

- O estado compartilhado é um mapa ou contador acessado por muitas goroutines com leitura/escrita curtas.
- A "mensagem" é na verdade um acesso a estado protegido, não um fluxo de dados.

Use `sync.WaitGroup` quando só precisa esperar um conjunto de goroutines terminar (sem dados fluindo entre elas). Use `errgroup` quando precisa do primeiro erro e cancelamento (já coberto no [guia de errgroup](/blog/errgroup-go-concorrencia-cancelamento-erros/)). Use `atomic` para contadores simples. Canais brilham em fluxo de dados e sinalização; em estado, mutex e atômicos ganham em clareza e performance.

Para times com múltiplas stacks, vale comparar como o <a href="https://python.dev.br/aprenda/concorrencia-go/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python Dev Brasil aborda concorrência</a>: asyncio e filas resolvem o mesmo problema de backpressure entre produtores e consumidores, mas o modelo de channels tipados e `select` do Go elimina uma categoria inteira de data race que em Python exige locks explícitos ou filas thread-safe.

## Conclusão

Channels são a parte da biblioteca padrão de Go que mais recompensa estudo, porque cada padrão (pipeline, fan-in, semáforo, done channel) vira um bloco reutilizável que compõe sem atrito. As regras que mantêm um serviço saudável são poucas e consistentes: remetente fecha, todo `select` respeita `ctx.Done()`, buffer é decisão de projeto e não remendo, e toda goroutine tem caminho de saída. Aplicadas junto com [context para cancelamento](/blog/context-timeout-cancelamento-go/) e [errgroup para fan-out com erro](/blog/errgroup-go-concorrencia-cancelamento-erros/), elas cobrem praticamente todo cenário de concorrência real em backend — sem um único mutex desnecessário.
