Aula 33 - Tutorial Golang - Iteração sobre Valores Recebidos de um Canal

Introdução:

Nesta aula, vamos explorar um conceito importante em programação concorrente em Go: a iteração sobre valores recebidos de um canal. Veremos como a sintaxe do for e range pode ser utilizada para percorrer os valores recebidos de um canal e como o fechamento do canal afeta essa iteração.

package main

import "fmt"

func main() {
    // Criando um canal com capacidade para 2 valores
    // Iremos iterar sobre 2 valores no canal `queue`
    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)

    // Iterando sobre os valores recebidos do canal usando `for` e `range`
    for elem := range queue {
        fmt.Println(elem)
    }
}

A função main é a entrada principal do programa

Nela criamos um canal chamado queue com capacidade para 2 valores usando make(chan string, 2). Em seguida, enviamos dois valores para o canal queue usando o operador <-. Fechamos o canal queue usando a função close(queue). Isso indica que nenhum valor adicional será enviado para o canal. Usamos um loop for e range para iterar sobre os valores recebidos do canal queue. O loop continua até que o canal seja fechado e todos os valores sejam recebidos. Em cada iteração, o valor recebido é armazenado na variável elem, e então o valor é impresso no console usando fmt.Println(elem). O resultado será a impressão dos valores "one" e "two" que foram enviados e recebidos do canal queue. Esse exemplo ilustra como utilizar o for e range para iterar sobre os valores recebidos de um canal em Go. É uma forma eficiente de processar os valores recebidos do canal de forma sequencial.

Processamento Paralelo

Caso de uso Ao usar goroutines para processamento paralelo, é comum utilizar canais para enviar e receber dados entre as goroutines. A iteração sobre os valores recebidos de um canal permite processar os resultados simultaneamente.

Import do sync 

Por causa do import do sync, antes de rodar o código precisamos executar: go mod init <modulename>, para limpar e atualizar o arquivo go.mod e o arquivo go.sum. Ele remove as dependências que não estão mais sendo utilizadas no projeto e adiciona as dependências necessárias com as versões corretas. É útil para garantir que as dependências do projeto estejam atualizadas e corretas. No meu caso, vou chamar o módulo de fluentcode.com.
go mod init fluentcode.com
Rode também o: go mod tidy para inicializar um módulo e gerenciar as dependências do projeto.
go mod tidy
Exemplo de código:

package main

import (
    "fmt"
    "sync"
)

func processWorker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        // Realize o processamento do trabalho aqui
    }
}

func main() {
    jobs := make(chan int, 5)
    var wg sync.WaitGroup

    numWorkers := 3
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go processWorker(i, jobs, &wg)
    }

    // Enviar trabalhos para os workers
    for i := 1; i <= 10; i++ {
        jobs <- i
    }
    close(jobs)

    // Aguardar a conclusão de todos os workers
    wg.Wait()
}
Neste exemplo, criamos goroutines (workers) que processam trabalhos em paralelo. Os trabalhos são enviados para o canal jobs e cada worker itera sobre os valores recebidos desse canal usando o for e range.

A saída do código será algo semelhante a:

Worker 1 processing job 1 Worker 2 processing job 2 Worker 3 processing job 3 Worker 1 processing job 4 Worker 2 processing job 5 Worker 3 processing job 6 Worker 1 processing job 7 Worker 2 processing job 8 Worker 3 processing job 9 Worker 1 processing job 10 No entanto, é importante ressaltar que a ordem exata pode variar a cada execução do programa devido à concorrência dos workers. Portanto, embora o primeiro worker comece a processar o job 1, não é garantido que ele terminará de processá-lo antes que outros workers iniciem seus próprios jobs. Além disso, como o canal jobs tem uma capacidade de 5, o envio de jobs além desse limite ficará em espera até que os workers processem alguns jobs existentes e liberem espaço no canal. Portanto, a ordem precisa de processamento e a distribuição exata dos jobs entre os workers podem variar a cada execução.

Função processWorker() do código acima

A função processWorker() representa a rotina de trabalho de cada worker. Recebe o ID do worker, um canal jobs de onde ele recebe os trabalhos a serem processados e um ponteiro para um objeto sync.WaitGroup para indicar que o worker concluiu seu trabalho. O loop: for job := range jobs, percorre o canal jobs e recebe os trabalhos à medida que são enviados. Em seguida, o worker processa o trabalho, que é simulado com a impressão de uma mensagem. O defer wg.Done() é usado para indicar que o worker terminou seu trabalho, decrementando o contador do sync.WaitGroup quando a função processWorker retorna.

Função main() do código acima

A função main() é responsável por configurar e coordenar os workers. Criamos um canal jobs com uma capacidade de 5 para armazenar os trabalhos a serem processados. Inicializamos uma variável wg do tipo sync.WaitGroup para sincronizar a finalização de todos os workers. Definimos o número de workers desejado (numWorkers = 3) e, em seguida, iniciamos goroutines chamando a função processWorker() para cada worker. Em seguida, enviamos 10 trabalhos para o canal jobs utilizando um loop for. Fechamos o canal jobs para indicar que nenhum trabalho adicional será enviado. Por fim, chamamos wg.Wait() para aguardar a conclusão de todos os workers, o que é indicado pelo wg.Done() em cada worker.

Consumo de Streams de Dados

Caso de uso Em situações em que os dados estão sendo transmitidos continuamente em um canal, a iteração sobre os valores recebidos permite consumir esses dados conforme eles estão disponíveis, em tempo real. Exemplo de código:

package main

import (
    "fmt"
    "time"
)

func dataProducer(data chan<- int) {
    for i := 1; i <= 5; i++ {
        data <- i
        time.Sleep(time.Second)
    }
    close(data)
}

func main() {
    data := make(chan int)

    go dataProducer(data)

    for value := range data {
        fmt.Println("Received:", value)
    }
}
Neste exemplo, temos uma goroutine dataProducer que envia valores para o canal data em intervalos de 1 segundo. A função main() itera sobre os valores recebidos desse canal usando o for e range, imprimindo os valores à medida que são recebidos. Esses são apenas dois casos de uso comuns em que a iteração sobre valores recebidos de um canal é aplicável. A versatilidade dessa abordagem permite seu uso em uma ampla gama de cenários de programação concorrente.

Conclusão

Dominar a iteração sobre valores recebidos de um canal é essencial para o desenvolvimento de programas concorrentes eficientes em Go. Ao compreender os conceitos abordados nesta aula, você estará um passo mais próximo de aproveitar todos os benefícios da programação concorrente em Go. Continue praticando e explorando os recursos dessa poderosa linguagem! #Golang #ProgramacaoConcorrente #IteracaoCanal #ForRange

Eu fico por aqui e agradeço a você pela audiência.

Até mais. :)

Página principal do blog

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Obrigado e bons estudos. ;)