Janeiro 2026 · ~10 min

Goroutines e Channels em Go

Guia completo de goroutines e channels em Go com exemplos: select, WaitGroup, context, worker pool e errgroup — e como evitar leaks e race conditions.

Goroutines e Channels em Go

Resposta rápida: goroutines (também buscadas como go routines ou go routine) são unidades leves de concorrência em Go (go f()). Go 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.

Como o scheduler de goroutines funciona (GMP e GOMAXPROCS)

O runtime do Go agenda goroutines com o modelo GMP, documentado no design do scheduler (comentários no topo do arquivo) e no pacote runtime:

  • G (goroutine): a unidade de trabalho — stack própria, começa pequena e cresce
  • M (machine): uma thread do sistema operacional
  • P (processor): o direito de executar Go código; cada P tem uma fila local de Gs prontas

Ms “pegam” Ps para rodar Gs. Quando a fila local esvazia, o runtime rouba metade da fila de outro P (work stealing). Chamadas bloqueantes (cgo, I/O pesado) desacoplam a M do P, e desde o Go 1.14 o scheduler também preempta goroutines em loops longos de CPU.

GOMAXPROCS define quantos Ps existem — ou seja, quantas goroutines executam simultaneamente em CPU. Desde o Go 1.5 o padrão é runtime.NumCPU():

fmt.Println(runtime.GOMAXPROCS(0)) // lê o valor atual sem alterar
fmt.Println(runtime.NumCPU())       // CPUs lógicas visíveis ao processo
fmt.Println(runtime.NumGoroutine()) // goroutines vivas agora

Caveat de produção: em containers com limite de CPU (cgroups), o runtime não lê a cota — GOMAXPROCS fica no número de CPUs do nó e a contenção de threads pode degradar latência. É o caso clássico para o go.uber.org/automaxprocs, que ajusta GOMAXPROCS a partir do cgroup no boot. O detalhe de quanto isso pesa depende do workload: meça com pprof antes e depois.

Panic em goroutine derruba o processo inteiro

Um panic não tratado em qualquer goroutine encerra o programa — não existe isolamento entre elas. E recover só captura se estiver deferido dentro da mesma goroutine:

// ERRADO — recover na main não captura panic da goroutine
func main() {
    defer func() { recover() }()
    go func() { panic("boom") }() // derruba o processo
    time.Sleep(time.Second)
}

// CORRETO — cada goroutine se protege
func worker(ctx context.Context) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("worker recuperado: %v", r)
            }
        }()
        fazTrabalho(ctx)
    }()
}

Em serviços, o padrão comum é: recover por goroutine + log com stack (debug.Stack()) + métrica de panic — e deixar o crash real para o supervisor/restart da plataforma.

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: 8 de outubro de 2026 — título e resumo reescritos para refletir o guia completo. Refresh de 01/10 aprofundou scheduler GMP, GOMAXPROCS em containers, panic/recover por goroutine e FAQ.

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().

Go routines e goroutines são a mesma coisa?

Sim. Go routines (ou go routine no singular) é a forma espaçada com que muita gente busca o conceito; o nome oficial na linguagem é goroutine (uma palavra). Em código, a sintaxe é a mesma: go f(). Channels, select, WaitGroup e context continuam iguais.

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().

O que é GOMAXPROCS em Go?

GOMAXPROCS controla quantas goroutines rodam ao mesmo tempo em CPU: define o número de processadores lógicos (P) que o scheduler usa. Desde o Go 1.5 o padrão é o número de CPUs lógicas da máquina. Leia com runtime.GOMAXPROCS(0) e ajuste com runtime.GOMAXPROCS(n); em containers com limite de CPU, o padrão pode ficar acima da cota.

O que acontece se uma goroutine entrar em panic?

Um panic não tratado em qualquer goroutine derruba o processo inteiro, mesmo que outras goroutines estejam saudáveis. recover só funciona se estiver deferido dentro da própria goroutine que panicou — um recover no chamador não captura panic de goroutine filha.