Série: Café com Go Parte 4

O momento 'Aha!': Como Goroutines e Channels mudaram minha cabeça

Postado em

O peso do passado

Antes de descobrir Go, eu vivia num campo minado de concorrência. Em Java eu me via rodeado de threads que surgiam como fantasmas, cada uma precisando de um lock ou mutex para não pisarem umas nas outras. No C++ a situação era ainda pior, a cada nova classe de sincronização eu precisava lembrar de chamar std::lock_guard, limpar o lock manualmente e rezar para que nenhum deadlock surgisse durante o teste de integração.

Essas experiências deixavam-me com a sensação de estar tentando conduzir um trem de carga usando apenas um chicote de cavalo. Cada tentativa de paralelismo terminava em corridas de dados, exceções inesperadas ou, pior ainda, em um core dump que aparecia como um aviso de “não tente isto em casa”.

A simplicidade do go

Então, chegou o dia em que eu escrevi a primeira linha que mudou tudo:

go fazerCafe()

Basta duas letras, go, e a função que antes bloqueava a execução principal passou a rodar em paralelo, como se eu tivesse acionado um botão “start” numa máquina de espresso automática. O compilador não reclamou, o runtime não travou, e o programa principal continuou a servir a próxima requisição.

Foi quase irresponsável sentir‑me tão à vontade para lançar milhares de goroutines sem medo de “estourar” o processo. A verdade é que o scheduler interno do Go cuida da multiplexação das goroutines sobre um pool pequeno de OS threads, de forma eficiente e transparente.

O conceito de Channels

A parte que realmente salvou a minha sanidade foi o Channel. Enquanto nas outras linguagens eu lutava para proteger variáveis globais com mutexes (e ainda assim acabava com race conditions), Go me ofereceu um tubo de comunicação onde os dados fluem de forma controlada.

// Cria um canal que transporta strings cafePronto := make(chan string) // Goroutine que prepara o café go func() { // Simula o tempo de preparo time.Sleep(2 * time.Second) cafePronto <- "☕ Espresso pronto" }() // Código principal espera a mensagem msg := <-cafePronto fmt.Println(msg) // → ☕ Espresso pronto

O canal age como o balcão da cafeteria: o barista (goroutine) coloca o café pronto no balcão, e o garçom (outra goroutine) o recolhe quando estiver pronto. Não há compartilhamento direto de memória; a única forma de “trocar” informações é através desse tubo.

Isso ecoa a máxima do Go:

“Do not communicate by sharing memory; instead, share memory by communicating.”

Ao seguir esse princípio, eliminei completamente a necessidade de variáveis globais perigosas e de locks que, antes, eram a fonte de tantos pesadelos.

O momento “Aha!”

Quando combinei goroutines com select e range sobre canais, o código ganhou uma clareza que eu nunca tinha visto em projetos concorrentes. Imagine um restaurante onde cada garçom tem seu próprio canal de pedidos; o chef pode atender a qualquer pedido que chegar primeiro, sem precisar verificar quem está esperando.

for { select { case msg := <-cafePronto: fmt.Println("Servindo:", msg) case <-time.After(5 * time.Second): fmt.Println("Nenhum café chegou, fechando a cozinha.") return } }

Esse padrão me fez sentir que, de alguma forma, eu havia adquirido um superpoder: a capacidade de coordenar múltiplas tarefas simultâneas sem medo de colisões, deadlocks ou corrupção de estado.

Conclusão

Passar de um cenário onde a concorrência era um campo minado para um ambiente onde basta lançar uma goroutine e comunicar‑se por canais foi, para mim, a epifania que justificou toda a curva de aprendizado do Go. Cada go que escrevo agora carrega consigo a confiança de que o runtime cuidará da orquestração, enquanto eu me concentro em modelar fluxos de dados claros e seguros.

No próximo capítulo, vamos encarar o velho inimigo dos desenvolvedores, o tratamento de erros. Veremos como o famigerado if err != nil pode, na verdade, ser o nosso melhor aliado.

Categoria: Backend, Engenharia de Sistemas
Gostou do conteúdo?

Se este artigo foi útil para você, considere apoiar meu trabalho!

Me compre um café

Comentários