Por que o Go superou todos em concorrência?
\n\nImagine um chef de cozinha preparando dez pratos ao mesmo tempo. Ele não corre de fogão em fogão como um louco — ele simplesmente alterna de forma eficiente entre as tarefas: enquanto a carne cozinha, corta a salada; enquanto a água ferve, embeleza o prato. No mundo da programação, isso se chama concorrência — a habilidade de gerenciar múltiplas tarefas sem esperar que cada uma termine.
\n\nA linguagem Go (Golang) nasceu exatamente para resolver problemas de concorrência. Seus criadores no Google se cansaram das complexidades do multithreading em C++ e Java. Eles criaram as goroutines — threads leves que são iniciadas com uma única linha de código. Neste artigo, vamos explorar como a concorrência funciona em Go, compará-la com threads do SO e com o paradigma async/await do Python e, o mais importante, mostrar exemplos reais de código que você pode aplicar hoje mesmo.
\n\nGoroutines vs Threads do SO: a batalha da cavalaria leve
\n\nQuando um desenvolvedor ouve "multithreading", geralmente pensa em threads do sistema operacional. Mas no Go é diferente. Vamos analisar as principais diferenças.
\n\nO que é uma Thread do SO?
\nUma thread é um objeto pesado gerenciado pelo kernel do SO. Cada thread tem sua própria pilha (geralmente 1-8 MB), exige chamadas de sistema para criação e alternância. Criar 10.000 threads significa sobrecarregar a memória e o processador. O Go adota uma filosofia diferente.
\n\nGoroutine — a thread leve do Go
\nUma goroutine é uma função executada concorrentemente. Ela é iniciada com a palavra-chave go. Principais características:
- \n
- Tamanho da pilha: começa com 2-4 KB e cresce/encolhe dinamicamente. \n
- Criação: milhares de goroutines são criadas em milissegundos. \n
- Escalonador: o runtime do Go distribui as goroutines entre as threads do SO (geralmente igual ao número de núcleos da CPU). \n
- Sintaxe: basta adicionar
goantes da chamada da função. \n
package main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc sayHello(name string) {\n for i := 0; i < 3; i++ {\n fmt.Println("Olá,", name)\n time.Sleep(100 * time.Millisecond)\n }\n}\n\nfunc main() {\n // Iniciando duas goroutines\n go sayHello("Go")\n go sayHello("Goroutine")\n\n // Dando tempo para as goroutines executarem\n time.Sleep(1 * time.Second)\n fmt.Println("Função principal finalizada")\n}\n\nResultado: as mensagens das duas goroutines serão intercaladas. O Go alterna automaticamente entre elas, sem bloqueio mútuo. Experimente executar este código — você verá "Olá, Go" e "Olá, Goroutine" sendo exibidos alternadamente.
\n\nCanais: a principal ferramenta de sincronização
\n\nGoroutines são ótimas, mas como elas se comunicam? No Go, não há memória compartilhada por padrão. Em vez disso, usa-se o princípio: "Não se comunique compartilhando memória; compartilhe memória se comunicando". Para isso, existem os canais (channels).
\n\nUm canal é um "tubo" tipado através do qual as goroutines enviam e recebem dados. É seguro, simples e elegante.
\n\nExemplo: processamento paralelo de números
\npackage main\n\nimport "fmt"\n\nfunc squareWorker(numbers <-chan int, results chan<- int) {\n for n := range numbers {\n results <- n * n\n }\n close(results)\n}\n\nfunc main() {\n numbers := make(chan int, 5)\n results := make(chan int, 5)\n\n // Iniciando o worker\n go squareWorker(numbers, results)\n\n // Enviando dados\n numbers <- 2\n numbers <- 4\n numbers <- 6\n close(numbers)\n\n // Lendo os resultados\n for res := range results {\n fmt.Println("Quadrado:", res)\n }\n}\n\nNeste exemplo, usamos canais com buffer (o segundo argumento é o tamanho do buffer). A goroutine worker lê do canal numbers, calcula o quadrado e envia o resultado para o canal results. O canal com buffer permite enviar dados sem que o receptor esteja imediatamente pronto, tornando a comunicação mais flexível.