Go Goroutines e Channels: Guia Prático de Concorrência

Backend2026-09-30TryQuickToolBox

Você provavelmente já escreveu código Go que funciona bem até que deixa de funcionar — talvez uma data race derrube seu serviço sob carga, ou um deadlock congele sua API. A concorrência é um dos maiores atrativos do Go, mas também é onde muitos desenvolvedores ficam travados. Goroutines e channels são os blocos de construção, mas usá-los corretamente exige entender seu comportamento e as armadilhas comuns.

Este guia apresenta padrões práticos para usar goroutines e channels, com exemplos de código e conselhos que você pode aplicar imediatamente.

O que são Goroutines?

Uma goroutine é uma thread leve gerenciada pelo runtime do Go. Você cria uma ao prefixar uma chamada de função com go:

go doSomething()

Goroutines começam com uma pilha pequena (alguns kilobytes) que cresce conforme necessário. Isso torna barato criar milhares ou até milhões delas. O escalonador do Go multiplexa goroutines em threads do sistema operacional, então você não gerencia threads diretamente.

Mas criar uma goroutine sem coordenação é uma receita para o caos. Você precisa de uma forma de esperar por elas, passar dados ou cancelá-las — é aí que entram os channels.

Channels: Comunicação Entre Goroutines

Channels são condutores tipados para enviar e receber valores. Eles fornecem sincronização: um envio bloqueia até que um receptor esteja pronto, e um recebimento bloqueia até que um valor esteja disponível. Isso torna os channels ideais para coordenar goroutines.

Crie um channel com make(chan T):

ch := make(chan int)
go func() {
    ch <- 42 // send
}()
value := <-ch // receive

Por padrão, os channels são sem buffer, o que significa que envios e recebimentos acontecem em compasso. Channels com buffer permitem um número limitado de envios sem um receptor:

ch := make(chan int, 3) // buffer size 3
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block until someone receives

Channels com buffer podem suavizar picos, mas não eliminam a necessidade de coordenação.

Padrões Comuns de Concorrência

1. Pool de Workers

Distribua tarefas entre um número fixo de workers para limitar o uso de recursos:

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        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 i := 1; i <= 5; i++ {
        jobs <- i
    }
    close(jobs)

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

Este padrão é ótimo para tarefas limitadas por CPU ou IO onde você quer controlar o paralelismo.

2. Fan-Out, Fan-In

Fan-out significa que várias goroutines leem do mesmo channel. Fan-in significa mesclar resultados de vários channels em um só:

func fanIn(chs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(chs))
    for _, ch := range chs {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Use isso quando você tem múltiplos produtores e quer um único fluxo de resultados.

3. Select para Multiplexação

A instrução select permite que uma goroutine aguarde múltiplas operações de channel:

select {
case msg := <-ch1:
    fmt.Println("received from ch1:", msg)
case ch2 <- 42:
    fmt.Println("sent to ch2")
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

Use select com um caso default para operações não bloqueantes, ou com um timeout para evitar esperar para sempre.

Armadilhas e Como Evitá-las

Boas Práticas

Comparação: Channels vs. Mutexes

AspectoChannelsMutexes
Caso de usoPassar dados, coordenaçãoProteger estado compartilhado
ComplexidadeMaior para tarefas simplesMenor para tarefas simples
DesempenhoOverhead de sincronizaçãoGeralmente mais rápido para seções críticas curtas
RiscoDeadlocks, vazamentosDeadlocks se não tomar cuidado

FAQ

Quando devo usar um channel com buffer?

Use um channel com buffer quando quiser desacoplar remetentes e receptores para lidar com picos, ou quando souber o número exato de envios e quiser evitar bloqueios. No entanto, channels sem buffer fornecem garantias de sincronização mais fortes e geralmente são mais fáceis de raciocinar.

Como evito vazamentos de goroutine?

Garanta que toda goroutine tenha um caminho de saída claro. Use context.Context para sinalizar cancelamento, feche channels quando não houver mais valores a enviar e evite bloquear indefinidamente em operações de channel sem um timeout ou caso default.

Posso usar channels para tudo?

Não. Embora channels sejam poderosos, eles adicionam overhead e complexidade. Para estado compartilhado simples, um mutex geralmente é mais direto e eficiente. O provérbio Go diz: "Não se comunique compartilhando memória; compartilhe memória se comunicando", mas isso não significa que channels são sempre a resposta.

Se você está depurando problemas de concorrência ou analisando logs dos seus serviços Go, o Nginx Log Analyzer pode ajudar você a identificar padrões e erros rapidamente.