Programação concorrente com channels em Go

1. Fundamentos da Concorrência em Go

Go foi projetado desde sua origem para lidar com concorrência de forma nativa e elegante. O coração desse modelo são as goroutines — funções ou métodos que executam concorrentemente com outras goroutines, dentro do mesmo espaço de endereçamento. Diferente de threads de sistema operacional, goroutines são extremamente leves (apenas alguns KB de pilha) e gerenciadas pelo runtime do Go.

func dizerOla() {
    fmt.Println("Olá, mundo concorrente!")
}

func main() {
    go dizerOla()  // executa em uma nova goroutine
    time.Sleep(time.Second) // espera para ver o resultado
}

Enquanto linguagens tradicionais usam locks, mutexes e variáveis de condição para sincronização, Go propõe um paradigma diferente: "não se comunique compartilhando memória; compartilhe memória se comunicando". Essa filosofia vem do modelo CSP (Communicating Sequential Processes), formalizado por Tony Hoare em 1978, e que Go implementa através de channels.

2. Channels: Tipos, Criação e Operações Essenciais

Channels são os tubos que conectam goroutines concorrentes. Você pode enviar valores de um lado e recebê-los do outro. Em Go, channels são tipados — cada channel só transporta um tipo específico de dado.

// Channel unbuffered (sem buffer)
ch := make(chan int)

// Channel buffered (com buffer de 3 posições)
chBuf := make(chan string, 3)

// Enviar dados
ch <- 42

// Receber dados
valor := <-ch

Channels unbuffered bloqueiam o envio até que outra goroutine esteja pronta para receber — isso cria um ponto de sincronização natural. Channels buffered permitem enviar até que o buffer esteja cheio, sem bloquear imediatamente.

Fechar um channel com close(ch) é importante para sinalizar que não haverá mais envios. O receptor pode detectar isso:

valor, ok := <-ch
if !ok {
    // channel foi fechado
}

3. Padrões de Comunicação com Channels

Fan-in

Múltiplas goroutines produtoras enviam dados para um único channel consumidor:

func produtor(id int, ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- id*100 + i
    }
}

func main() {
    ch := make(chan int, 10)
    go produtor(1, ch)
    go produtor(2, ch)

    for i := 0; i < 10; i++ {
        fmt.Println(<-ch)
    }
}

Fan-out

Uma goroutine distribui trabalho para múltiplos workers concorrentes:

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

Pipeline

Encadeamento de stages onde a saída de um é a entrada do próximo:

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

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

4. Sincronização e Controle de Fluxo

Channels podem atuar como semáforos para limitar concorrência:

func main() {
    sem := make(chan struct{}, 3) // permite até 3 goroutines simultâneas

    for i := 0; i < 10; i++ {
        go func(id int) {
            sem <- struct{}{} // adquire slot
            defer func() { <-sem }() // libera slot

            fmt.Printf("Trabalhando %d\n", id)
            time.Sleep(time.Second)
        }(i)
    }
}

Para timeouts, combinamos select com time.After:

ch := make(chan int)
select {
case resultado := <-ch:
    fmt.Println("Resultado:", resultado)
case <-time.After(2 * time.Second):
    fmt.Println("Timeout!")
}

5. Select: Gerenciando Múltiplos Channels

A estrutura select permite que uma goroutine aguarde múltiplas operações de channel simultaneamente:

select {
case msg1 := <-ch1:
    fmt.Println("Recebido de ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("Recebido de ch2:", msg2)
case ch3 <- 42:
    fmt.Println("Enviado 42 para ch3")
default:
    fmt.Println("Nenhum canal pronto")
}

O default torna o select non-blocking — se nenhum canal estiver pronto, executa o caso default imediatamente. Isso é útil para implementar polling ou evitar deadlocks.

6. Boas Práticas e Armadilhas Comuns

Deadlocks ocorrem frequentemente com channels unbuffered quando não há correspondência entre envios e recebimentos:

// Deadlock! Envio bloqueia, mas ninguém recebe
ch := make(chan int)
ch <- 42  // deadlock

Vazamento de goroutines acontece quando uma goroutine fica esperando eternamente em um channel que nunca será fechado ou receberá dados. O padrão "done channel" resolve isso:

func main() {
    done := make(chan struct{})

    go func() {
        // trabalho...
        close(done) // sinaliza conclusão
    }()

    <-done // aguarda finalização
}

Sempre feche channels do lado do produtor, nunca do consumidor. E evite fechar channels que já estão sendo usados por múltiplos produtores — nesse caso, use um channel de "done" separado.

7. Casos de Uso Avançados

Workers Pool com Channels

func workerPool(tasks []int, numWorkers int) []int {
    taskCh := make(chan int, len(tasks))
    resultCh := make(chan int, len(tasks))

    // Inicia workers
    for w := 0; w < numWorkers; w++ {
        go func() {
            for task := range taskCh {
                resultCh <- task * task
            }
        }()
    }

    // Envia tarefas
    for _, task := range tasks {
        taskCh <- task
    }
    close(taskCh)

    // Coleta resultados
    var results []int
    for i := 0; i < len(tasks); i++ {
        results = append(results, <-resultCh)
    }
    return results
}

Rate Limiting com Channel Ticker

func rateLimitedProcess(requests []int) {
    ticker := time.NewTicker(200 * time.Millisecond)
    defer ticker.Stop()

    for _, req := range requests {
        <-ticker.C // aguarda o tick
        go func(r int) {
            fmt.Println("Processando:", r)
        }(req)
    }
}

Orquestração com Context e Channels

func processWithTimeout(ctx context.Context, ch chan int) {
    select {
    case val := <-ch:
        fmt.Println("Valor recebido:", val)
    case <-ctx.Done():
        fmt.Println("Cancelado:", ctx.Err())
    }
}

Conclusão

Channels em Go oferecem um modelo de concorrência poderoso e seguro, baseado em comunicação em vez de memória compartilhada. Dominar padrões como fan-in, fan-out, pipelines e o uso correto de select permite construir sistemas concorrentes robustos, evitando deadlocks e vazamentos de goroutines. A simplicidade do modelo CSP em Go torna a concorrência acessível sem sacrificar desempenho ou segurança.

Referências