Janeiro 2026 · ~8 min

Concorrência em Go: Goroutines e Channels

Goroutines em Go: channels, select, WaitGroup, context, worker pool e race detector. Exemplos + checklist de produção para backend e entrevistas.

Concorrência em Go: Goroutines e Channels

Resposta rápida: em Go, goroutines são unidades leves de concorrência (go f()), channels transportam valores entre elas e select multiplexa waits. Junte sync.WaitGroup ou errgroup, context.Context para cancelar e go test -race para caçar corridas. Com isso você cobre o núcleo pedido em APIs, workers e entrevistas — sem framework.

Este guia é o ponto de entrada prático de concorrência no golang.com.br: o modelo mental, exemplos que compilam, armadilhas (leak, race, close errado) e o caminho para os guias de produção — channels, errgroup, worker pool, sync.Mutex / WaitGroup / RWMutex / Once e testes com -race.

Goroutines vs threads vs async (visão rápida)

ModeloCusto típicoQuem agendaEncaixa bem quando
Thread OS~1 MB stack, create caroKernelPoucas tarefas bloqueantes nativas
Goroutine (Go)~2 KB stack inicial, create baratoRuntime Go (M:N)APIs, I/O, fan-out, workers
Async/await (outras langs)Stacks lógicas / futuresEvent loop + runtimeEcossistemas já async-first

Go não exige anotar cada chamada com async. Você marca o ponto de concorrência com go e usa channels/context para coordenar. Para comparar com ownership/Tokio, veja Go vs Rust; para o mercado backend, Go para backend.

Goroutines

Uma goroutine é uma função executando concorrentemente com outras. Para criar, use a palavra-chave go:

package main

import (
    "fmt"
    "time"
)

func dizer(msg string) {
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go dizer("goroutine") // executa concorrentemente
    dizer("main")         // executa na goroutine principal
    time.Sleep(500 * time.Millisecond)
}

Características das goroutines

  • Leves: stack inicial pequena (~2 KB), cresce conforme a necessidade
  • Multiplexadas: milhares podem rodar em poucas threads OS
  • Gerenciadas pelo runtime: o scheduler do Go troca de contexto; desde versões recentes há preempção mais agressiva em loops longos
// Criar muitas goroutines é barato — mas cada uma ainda precisa terminar
for i := 0; i < 1000; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

Armadilha de captura de loop: antes do Go 1.22, o i do for era reutilizado e todas as goroutines podiam ver o mesmo valor. Em Go 1.22+ cada iteração tem sua própria variável. Mesmo assim, passar n int como argumento continua sendo o estilo mais explícito e seguro em reviews.

Channels

Channels são a forma idiomática de comunicação entre goroutines. O ditado oficial:

“Não comunique compartilhando memória; compartilhe memória comunicando.”

Criando e usando channels

ch := make(chan int)

go func() {
    ch <- 42 // envia
}()

valor := <-ch // recebe
fmt.Println(valor) // 42

Channels bufferizados

ch := make(chan int, 3)

ch <- 1 // não bloqueia
ch <- 2
ch <- 3
// ch <- 4  // bloquearia: buffer cheio

Buffer não “resolve” race condition: só muda quando o send bloqueia. Use buffer para suavizar rajadas (jobs, logs), não como desculpa para não desenhar backpressure.

Direção de channels

func produtor(ch chan<- int) { ch <- 1 }

func consumidor(ch <-chan int) {
    valor := <-ch
    _ = valor
}

Restringir a direção no tipo da função documenta o contrato e evita close acidental no consumidor.

Fechando channels

ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // só o produtor fecha
}()

for v := range ch {
    fmt.Println(v)
}
valor, ok := <-ch
if !ok {
    fmt.Println("channel fechado")
}

Regra: quem envia fecha. Nunca feche do lado receptor. Fechar duas vezes ou enviar em channel fechado gera panic.

Select

select espera o primeiro case pronto:

select {
case msg1 := <-ch1:
    fmt.Println("ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("ch2:", msg2)
case ch3 <- valor:
    fmt.Println("enviou para ch3")
default:
    fmt.Println("nenhum channel pronto")
}

Timeout e cancelamento

select {
case res := <-ch:
    fmt.Println(res)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

Em produção, prefira context a time.After solto em loops quentes (cada After aloca um timer). Padrão idiomático:

select {
case res := <-ch:
    return res, nil
case <-ctx.Done():
    return 0, ctx.Err()
}

O guia context.Context: timeout e cancelamento aprofunda deadlines em HTTP, DB e workers.

sync.WaitGroup (e quando subir para errgroup)

Para esperar N goroutines terminarem sem channel de resultados:

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}

wg.Wait()

Chame Add antes de go. Done no defer. Para fan-out com cancelamento no primeiro erro, troque o WaitGroup manual por errgroup. Para o primer completo de Mutex, RWMutex, Once e WaitGroup, use o guia de sync.

Padrões comuns

Worker pool

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d processando job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= 5; a++ {
        <-results
    }
}

Em produção você ainda precisa de: context, tamanho de fila, métricas, graceful shutdown e teste sem time.Sleep. O deep dive está em Worker Pool em Go. Para filas externas (SQS, Redis Streams, cron), veja também SQS workers e cron/jobs agendados.

Fan-out / Fan-in

func fanIn(inputs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

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

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

    return out
}

Quando o fan-out chama APIs e deve cancelar tudo no primeiro erro, errgroup.WithContext costuma ser mais claro que WaitGroup + channel de erro manual.

Pipeline

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

func quadrado(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

func main() {
    for v := range quadrado(gerador(1, 2, 3, 4)) {
        fmt.Println(v) // 1, 4, 9, 16
    }
}

Pipelines brilham em ETL, parsers e processamento em estágios. Cada estágio fecha o próprio output; o estágio seguinte faz range.

Deduplicar trabalho repetido (cache stampede)

Se N goroutines disparam o mesmo fetch caro ao mesmo tempo, singleflight deduplica a chamada. Não substitui channel — complementa cache. Veja singleflight e cache stampede.

Context: o fio que segura produção

Quase todo serviço Go real passa ctx adentro:

  1. Handler HTTP recebe r.Context()
  2. Propaga para DB, HTTP client e workers
  3. No shutdown, cancela o contexto raiz (veja graceful shutdown)

Checklist mínimo em código concorrente:

  • Toda goroutine de longa duração observa ctx.Done()
  • Timeouts usam context.WithTimeout / WithDeadline
  • Não guarde Context em structs por tempo indefinido sem motivo claro
  • Cancele ao retornar erro — não deixe fan-out órfão

Armadilhas comuns

1. Race condition

// ERRADO
counter := 0
for i := 0; i < 1000; i++ {
    go func() { counter++ }()
}

// CORRETO — mutex (ou atomic.Int64)
var mu sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
    go func() {
        mu.Lock()
        counter++
        mu.Unlock()
    }()
}

2. Goroutine leak

// ERRADO — ninguém envia, ninguém cancela
func leak() {
    ch := make(chan int)
    go func() {
        <-ch
    }()
}

// CORRETO
func semLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case <-ch:
        case <-ctx.Done():
            return
        }
    }()
}

3. Fechar channel do lado errado

Só o produtor fecha. Consumidores fazem range ou v, ok := <-ch.

4. Send sem buffer e sem receptor

Send em channel unbuffered bloqueia até alguém receber. Em shutdown, isso vira deadlock se o receptor já saiu. Prefira select com ctx.Done() ou buffer dimensionado + abandono explícito.

Detectando race conditions

go test -race ./...
go build -race -o app ./cmd/api

O detector só vê caminhos exercitados. Combine com table-driven tests, flags de go test (-count, -shuffle) e, em hot paths, pprof / go tool trace para ver stalls e goroutines presas.

Checklist de produção (concorrência)

ItemPor quê
context em bordas (HTTP, jobs)Cancelamento e timeout únicos
Limite de fan-out (errgroup.SetLimit / worker pool)Evita abrir 10k conexões
Backpressure (buffer finito + bloqueio)Protege memória
-race no CI de pacotes críticosPega data races cedo
Métricas: fila, workers busy, tempo de waitOpera o sistema, não só o código
Shutdown: pare de aceitar → drena → cancelaDeploy sem matar request no meio

Concorrência na carreira Go

Em perguntas de entrevista e vagas, cobram menos “o que é goroutine?” e mais:

  • Diferença channel vs mutex
  • Como você cancela um fan-out
  • Como testa race sem flaky sleep
  • Quando worker pool local perde para fila (Kafka/SQS)

Monte um projeto pequeno (API + worker) no portfólio e saiba narrar o desenho. Para remuneração e senioridade, cruze com salários Go e plano de carreira.

Recursos oficiais

Para outra lente sobre o mesmo problema, use o comparativo Go vs Rust e os artigos irmãos Rust vs Go no Rust Brasil e structured concurrency em Kotlin.


Última atualização: Agosto 2026

Veja também

Perguntas frequentes

O que são goroutines em Go?

Goroutines são funções leves executadas concorrentemente pelo runtime do Go. Custam cerca de 2 KB de stack inicial (muito menos que uma thread do sistema) e são multiplexadas em poucas threads OS. Você cria uma com go func() ou go minhaFunc().

Qual a diferença entre goroutines e threads?

Threads OS têm stack grande (~1 MB) e custo alto de criação. Goroutines usam o modelo M:N: milhares delas rodam sobre poucas threads, com scheduling cooperativo/preemptivo do runtime. Iniciam rápido, escalam melhor e combinam com channels e context para cancelamento.

Quando usar channel em vez de mutex?

Prefira channel quando a história é passar ownership de dados ou sinalizar eventos entre goroutines (pipeline, fan-in, jobs). Prefira mutex/atomic quando várias goroutines precisam atualizar o mesmo estado compartilhado com seções críticas curtas. O guia de sync.Mutex e WaitGroup aprofunda essa escolha.

Como evitar goroutine leak?

Toda goroutine precisa de um caminho de saída. Passe context.Context, selecione ctx.Done() junto com o channel de trabalho, feche channels do lado produtor e evite bloquear para sempre em sends/receives sem timeout ou cancelamento. Em testes, use go test -race e falhe se o processo não terminar.

O que é select em Go?

select espera o primeiro case de channel pronto (receber, enviar ou timeout). Com default vira non-blocking. É a base de multiplexação, timeouts com time.After ou context, e cancelamento limpo. Sem select, código concorrente vira uma árvore frágil de locks.

Goroutines caem em entrevista de emprego Go?

Sim. Vagas de backend Go no Brasil pedem com frequência goroutines, channels, select, WaitGroup, context, race detector e padrões worker pool/pipeline. Treine explicar trade-offs e detectar leaks — não só decorar a sintaxe go func().