Channels são um dos pilares da concorrência em Go. Eles permitem que goroutines troquem dados de forma segura e sincronizada, evitando condições de corrida. Nesta aula, vamos explorar os fundamentos dos channels, desde sua criação até padrões avançados de uso, com exemplos práticos.

Entender channels é essencial para escrever programas concorrentes eficientes em Go. Eles funcionam como tubos por onde valores fluem, podendo ser usados para sincronizar execuções ou comunicar resultados entre goroutines.

Criação

Um channel em Go é criado com a função make, especificando o tipo de dado que será transmitido. A sintaxe é ch := make(chan Tipo). Por exemplo, ch := make(chan int) cria um channel de inteiros. Channels sem buffer são chamados de unbuffered e exigem que uma goroutine esteja pronta para receber o valor no momento do envio, garantindo sincronização.

Exemplo de criação e uso básico:

package main

import "fmt"

func main() {
    ch := make(chan int)
    go func() {
        ch <- 42
    }()
    valor := <-ch
    fmt.Println(valor)
}

Neste exemplo, criamos um channel de inteiros, enviamos o valor 42 em uma goroutine separada e o recebemos na main. A operação de envio bloqueia até que o recebimento ocorra.

send e receive

As operações de envio (send) e recebimento (receive) são a base da comunicação via channels. O envio é feito com ch <- valor e o recebimento com valor := <-ch. Em channels unbuffered, essas operações são bloqueantes: um envio espera um recebedor e vice-versa. Isso permite sincronizar goroutines.

Exemplo de sincronização:

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    fmt.Print("Trabalhando...")
    time.Sleep(time.Second)
    fmt.Println("pronto")
    done <- true
}

func main() {
    done := make(chan bool)
    go worker(done)
    <-done
    fmt.Println("Main finalizou")
}

Aqui, a goroutine worker sinaliza que terminou enviando true pelo channel. A main aguarda esse sinal, garantindo que o worker termine antes de prosseguir.

Channels com buffer

Channels podem ter um buffer, permitindo enviar valores sem que uma goroutine receptora esteja imediatamente pronta. A criação é ch := make(chan Tipo, capacidade). O buffer armazena até capacidade valores. Enquanto o buffer não estiver cheio, o envio não bloqueia; enquanto não estiver vazio, o recebimento não bloqueia.

Exemplo:

package main

import "fmt"

func main() {
    ch := make(chan string, 2)
    ch <- "olá"
    ch <- "mundo"
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Neste caso, enviamos duas strings sem necessidade de um receptor imediato. O buffer comporta 2 valores, então os envios não bloqueiam. Depois, recebemos ambos.

Channels com buffer são úteis para desacoplar produtores e consumidores, permitindo picos de produção sem bloquear. No entanto, é importante dimensionar o buffer adequadamente para evitar deadlocks.

Direção de channel

Go permite especificar a direção de um channel em parâmetros de função: chan<- para apenas envio e <-chan para apenas recebimento. Isso aumenta a segurança de tipo, garantindo que a função não use o channel indevidamente.

Exemplo:

package main

import "fmt"

func ping(pings chan<- string, msg string) {
    pings <- msg
}

func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "mensagem passada")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

A função ping só pode enviar para o channel, pong recebe de um e envia para outro. Isso documenta a intenção e previne erros de uso.

Boas práticas: Use channels unbuffered para sincronização e buffered para comunicação assíncrona controlada. Prefira definir direções nos parâmetros para clareza. Evite buffers grandes sem necessidade.

Referências

Exercícios

  1. Crie um programa que use um channel unbuffered para enviar um inteiro de uma goroutine para a main. Imprima o valor recebido.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int)
        go func() {
            ch <- 10
        }()
        fmt.Println(<-ch)
    }
  2. Implemente dois workers que processam dados de um channel bufferizado. Crie um channel com buffer de 3, envie 3 números e faça dois workers receberem concorrentemente.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
        defer wg.Done()
        for j := range jobs {
            fmt.Printf("Worker %d processou job %d\n", id, j)
        }
    }
    
    func main() {
        jobs := make(chan int, 3)
        var wg sync.WaitGroup
        for w := 1; w <= 2; w++ {
            wg.Add(1)
            go worker(w, jobs, &wg)
        }
        for j := 1; j <= 3; j++ {
            jobs <- j
        }
        close(jobs)
        wg.Wait()
    }
  3. Escreva uma função que receba um channel de envio (chan<- string) e envie uma mensagem. Depois, crie um channel e chame a função.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func sendMessage(ch chan<- string, msg string) {
        ch <- msg
    }
    
    func main() {
        ch := make(chan string, 1)
        sendMessage(ch, "Olá, mundo!")
        fmt.Println(<-ch)
    }
  4. Crie um programa que use um channel de recebimento (<-chan bool) para esperar que uma goroutine termine. A goroutine deve dormir 1 segundo antes de enviar o sinal.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func worker(done chan<- bool) {
        time.Sleep(1 * time.Second)
        done <- true
    }
    
    func main() {
        done := make(chan bool)
        go worker(done)
        <-done
        fmt.Println("Goroutine terminou")
    }
  5. Implemente um pipeline simples: uma goroutine gera números de 1 a 5 e envia para um channel; outra goroutine recebe e imprime o dobro. Use um channel bufferizado com buffer 2.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func generate(out chan<- int) {
        for i := 1; i <= 5; i++ {
            out <- i
        }
        close(out)
    }
    
    func double(in <-chan int, out chan<- int) {
        for v := range in {
            out <- v * 2
        }
        close(out)
    }
    
    func main() {
        nums := make(chan int, 2)
        result := make(chan int, 2)
        go generate(nums)
        go double(nums, result)
        for r := range result {
            fmt.Println(r)
        }
    }