Concorrência e Multithreading em Go: Nível Avançado
Go (Golang) foi projetado desde o início como uma linguagem para escrever aplicações escaláveis e de alto desempenho. Na base disso está um poderoso modelo de concorrência, fundamentado em goroutines e canais. Se você já domina os exemplos básicos com go func() e canais simples, é hora de avançar para o próximo nível. Neste artigo, exploraremos padrões avançados de trabalho com concorrência em Go que o ajudarão a escrever código confiável, eficiente e seguro.
1. Gerenciamento do Ciclo de Vida das Goroutines: Context e WaitGroup
Um dos problemas mais comuns é o vazamento de goroutines. Uma goroutine que espera infinitamente por dados de um canal nunca será finalizada se o canal for fechado incorretamente. Para gerenciar o ciclo de vida das goroutines em Go, existem duas ferramentas principais: sync.WaitGroup e context.Context.
sync.WaitGroup para Aguardar a Conclusão
Use sync.WaitGroup quando precisar aguardar a conclusão de um conjunto fixo de goroutines. É um mecanismo simples e eficaz.
package main
import (\t"fmt"\t"sync"\t"time")
func worker(id int, wg *sync.WaitGroup) {\tdefer wg.Done() // Decrementa o contador ao sair\tfmt.Printf("Worker %d iniciou o trabalho", id)\ttime.Sleep(time.Second)\tfmt.Printf("Worker %d finalizou o trabalho", id)}
func main() {\tvar wg sync.WaitGroup
\tfor i := 1; i <= 3; i++ {\t\twg.Add(1) // Incrementa o contador antes de iniciar\t\tgo worker(i, &wg)\t}
\twg.Wait() // Bloqueia até o contador chegar a 0\tfmt.Println("Todos os workers concluíram")}context.Context para Cancelamento e Prazos
Quando uma goroutine opera em um loop ou executa uma operação longa, ela precisa ser capaz de ser finalizada corretamente por um sinal externo. context.Context é a forma padrão de transmitir um sinal de cancelamento, prazo ou tempo limite.
package main
import (\t"context"\t"fmt"\t"time")
func longRunningTask(ctx context.Context) {\tfor {\t\tselect {\t\tcase <-ctx.Done():\t\t\tfmt.Println("Goroutine recebeu sinal de parada")\t\t\treturn\t\tdefault:\t\t\ttime.Sleep(500 * time.Millisecond)\t\t\tfmt.Println("Trabalhando...")\t\t}\t}}
func main() {\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\tdefer cancel() // Importante chamar cancel para liberar recursos
\tgo longRunningTask(ctx)
\ttime.Sleep(3 * time.Second)\tfmt.Println("Main finalizou")}2. Canais: Bufferização, Fechamento e Padrão Fan-Out/Fan-In
Canais são a principal forma de sincronização e transferência de dados entre goroutines. Em nível avançado, é importante entender a diferença entre canais com buffer e sem buffer, fechá-los corretamente e usar padrões de agregação.
Com Buffer vs Sem Buffer
Um canal sem buffer (sem buffer) bloqueia o remetente até que o destinatário leia os dados. Um canal com buffer permite enviar várias mensagens sem esperar, até que o buffer esteja cheio. A escolha depende da tarefa: para sincronização rigorosa, use canais sem buffer; para suavizar a carga, use canais com buffer.
Padrão Fan-Out/Fan-In
Este é um dos padrões mais poderosos. Fan-Out é a distribuição de tarefas entre várias goroutines (workers). Fan-In é a coleta de resultados de várias goroutines em um único canal.
package main
import (\t"fmt"\t"sync")
func producer(nums ...int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor _, n := range nums {\t\t\tout <- n\t\t}\t\tclose(out)\t}()\treturn out}
func square(in <-chan int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor n := range in {\t\t\tout <- n * n\t\t}\t\tclose(out)\t}()\treturn out}
func merge(channels ...<-chan int) <-chan int {\t