Goroutines e channels

Jean Franzen
4 min readNov 18, 2024

--

Quando falamos sobre goroutines, é importante lembrar que a linguagem Go é simples e dá bastante ênfase a processos concorrentes. A concorrência, por sua vez, é a capacidade de trabalhar com várias tarefas de forma independente, uma após a outra, o que é diferente do paralelismo, onde as tarefas são executadas literalmente ao mesmo tempo. Já abordei a diferença entre concorrência e paralelismo em um artigo anterior, então não vou me aprofundar nisso aqui.

Sem mais delongas, vamos entrar no tema de goroutines no Go, que é uma forma de implementar processos concorrentes no código. Elas são ideais para situações onde queremos aumentar a performance sem abrir mão da segurança e integridade dos dados. É aqui que entra a combinação de goroutines e channels.

Neste artigo, o foco será mais técnico, explorando exemplos práticos de código em vez de explicações teóricas.

Resumidamente, quando usamos a palavra-chave go no Go, estamos indicando que a execução de determinada função deve ser feita de forma concorrente. Por exemplo, se tivermos três funções que não dependem umas das outras, podemos executar todas de forma concorrente em vez de esperar que uma termine antes de iniciar a próxima.

Veja o exemplo abaixo, onde utilizamos goroutines para realizar três operações simultâneas:

package main

import (
"fmt"
"math/rand"
"sync"
"time"
)

// Simula um cálculo pesado
func performCalculation(id int, wg *sync.WaitGroup) {
defer wg.Done() // Indica que a goroutine terminou
fmt.Printf("Goroutine %d: Iniciando cálculo...\n", id)
time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // Simula tempo de processamento
fmt.Printf("Goroutine %d: Cálculo concluído.\n", id)
}

// Simula leitura de uma API externa
func fetchData(id int, wg *sync.WaitGroup) {
defer wg.Done() // Indica que a goroutine terminou
fmt.Printf("Goroutine %d: Iniciando leitura de API...\n", id)
time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // Simula tempo de resposta da API
fmt.Printf("Goroutine %d: Dados recebidos da API.\n", id)
}

// Simula gravação de logs
func writeLog(id int, wg *sync.WaitGroup) {
defer wg.Done() // Indica que a goroutine terminou
fmt.Printf("Goroutine %d: Iniciando gravação de logs...\n", id)
time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // Simula tempo de escrita
fmt.Printf("Goroutine %d: Logs gravados.\n", id)
}

func main() {
rand.Seed(time.Now().UnixNano()) // Garante aleatoriedade nos tempos

var wg sync.WaitGroup // Cria um WaitGroup

// Adiciona 3 ao contador do WaitGroup
wg.Add(3)

// Inicia as 3 goroutines concorrentes
go performCalculation(1, &wg)
go fetchData(2, &wg)
go writeLog(3, &wg)

// Aguarda todas as goroutines terminarem
wg.Wait()

fmt.Println("Todas as tarefas foram concluídas.")
}
Goroutine 3: Iniciando gravação de logs...
Goroutine 1: Iniciando cálculo...
Goroutine 1: Cálculo concluído.
Goroutine 2: Iniciando leitura de API...
Goroutine 2: Dados recebidos da API.
Goroutine 3: Logs gravados.
Todas as tarefas foram concluídas.

Começamos primeiro pela função Main Inicio dos processos

Criamos a variável para wg para utilizar os métodos de controle e sincronizar as goroutines

Na sequência utilizando o método add para inferir quantas goroutines vamos controlar pelo waitGroup

Utilizando a palavra go para informar que é uma goroutines e deve acontecer de forma concorrente

As três funções só simulam casos de uso de funções que podem ocorrer de forma concorrente sem nenhum problema, utilizando o método Done, do waitGroup junto com o Defer para indicar que sempre que acabar a função ele indique que aquela goroutine está completa.

pelo retorno indicado a cima conseguimos verificar que as três goroutines, acontecem de forma concorrente e utilizar de uma maneira mais performática mas segura do mesmo modo, melhor do que chamar cada função e esperar até a outra acabar…

Porém quando existem outros casos que também precisamos utilizar concorrência, porém temos funções que dependem de uma da outra porém não queremos abrir mão de uma boa perfomece de código e também da integridade de dados, podemos criar duas go routines e um channel entre elas, no exemplo abaixo podemos verificar o uso desse caso.

package main

import (
"context"
"fmt"
"sync"
"time"
)

func process(ctx context.Context, ch chan int, wg *sync.WaitGroup) {
defer wg.Done()

for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
fmt.Println("process canceled")
return
case ch <- i:
fmt.Println("sending", i)
time.Sleep(500 * time.Millisecond)
}
}

close(ch)

}

func consumer(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()

for v := range ch {
fmt.Println("received", v)
time.Sleep(1 * time.Second)
}
}

func main() {

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

defer cancel()

ch := make(chan int)

wg := sync.WaitGroup{}

wg.Add(2)

go process(ctx, ch, &wg)

go consumer(ch, &wg)

wg.Wait()

}
sending 0
received 0
sending 1
received 1
received 2
sending 2
received 3
sending 3
received 4
sending 4
process canceled
fatal error: all goroutines are asleep - deadlock!

Na função main, começamos criando um contexto para demonstrar o uso de context.WithTimeout, que permite definir quanto tempo uma operação pode levar antes de ser automaticamente cancelada. Usamos a função defer para garantir que o método cancel seja chamado ao final da execução da função principal, liberando os recursos associados ao contexto.

Também criamos um channel utilizando a palavra-chave make. Dentro dos parênteses, especificamos que é um canal e inferimos o tipo de dado que será trafegado por ele.

Assim como no exemplo anterior, utilizamos o WaitGroup para controlar o fluxo das goroutines, garantindo que o programa principal aguarde o término de todas antes de continuar.

Em seguida, declaramos a função process, responsável por processar os dados. Essa função recebe três parâmetros: o contexto (ctx), o WaitGroup (wg) e o canal (ch).

Dentro da função process, há um laço for que itera até que i seja menor que 10. No entanto, utilizamos um select com dois case:

  1. Quando o contexto expira (atingindo o tempo limite de 5 segundos definido na função main), a execução é interrompida.
  2. Quando o processamento é concluído, os valores processados são enviados para o canal.

Depois disso, utilizamos a função consumer, que itera sobre os valores presentes no canal utilizando um for com range. Isso demonstra que os dados enviados para o canal na função process são consumidos, simulando o comportamento de uma fila. Assim, criamos um fluxo concorrente entre duas funções dependentes: uma produz os dados e a outra os consome.

De forma resumida, sempre que possível, analise seu código e identifique áreas onde melhorias de desempenho podem ser feitas. Usar goroutines em concorrência e canais para comunicação entre elas pode trazer um bom ganho de processamento, garantindo que seus dados sejam performáticos e íntegros.

--

--

Jean Franzen
Jean Franzen

No responses yet