O que são Goroutines
Goroutines são funções que executam de forma concorrente dentro do mesmo espaço de memória da aplicação Go. Diferentemente de threads do sistema operacional, que consomem recursos significativos, goroutines são extremamente leves — você pode criar milhares delas sem degradação de performance. Elas são gerenciadas pelo runtime do Go, que é responsável por agendar sua execução nos processadores disponíveis.
A palavra-chave go é tudo que você precisa para iniciar uma goroutine. Quando você prefixar uma chamada de função com go, o runtime enfileira essa função para execução concorrente e retorna imediatamente ao código que a chamou. Isso permite que seu programa continue executando outras operações enquanto aquela goroutine roda em paralelo.
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 1; i <= 3; i++ {
fmt.Printf("Olá %s #%d\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sayHello("Alice")
go sayHello("Bob")
// Sem sleep, o programa encerraria antes das goroutines terminarem
time.Sleep(1 * time.Second)
fmt.Println("Programa finalizado")
}
Neste exemplo, duas goroutines executam simultaneamente, mas não há verdadeiro paralelismo se você tiver apenas um núcleo de processador. O runtime alterna entre elas, permitindo concorrência. O time.Sleep no main é uma solução temporária — mais adiante você aprenderá padrões mais elegantes.
Criação e Sincronização de Goroutines
Canais: A Forma Correta de Sincronizar
Canais são o mecanismo principal de comunicação entre goroutines em Go. Um canal é um tipo que permite que uma goroutine envie um valor que outra goroutine recebe. Quando você tenta receber de um canal vazio, a goroutine fica bloqueada até que algo seja enviado — esse é o segredo para sincronização sem usar locks explícitos.
package main
import (
"fmt"
)
func worker(id int, results chan string) {
// Simula algum trabalho
fmt.Printf("Worker %d iniciado\n", id)
results <- fmt.Sprintf("Worker %d completou", id)
}
func main() {
results := make(chan string, 3)
go worker(1, results)
go worker(2, results)
go worker(3, results)
// Bloqueia até receber 3 mensagens
for i := 0; i < 3; i++ {
fmt.Println(<-results)
}
fmt.Println("Todos os workers terminaram")
}
Quando você cria um canal com make(chan string, 3), o 3 é a capacidade do buffer. Com buffer, a goroutine pode enviar até 3 valores sem bloquear. Se a capacidade fosse 0 (ou omitida), o canal seria unbuffered, e o remetente bloquearia até que alguém recebesse.
WaitGroup: Padrão Padrão para Múltiplas Goroutines
Para cenários onde você tem um número variável de goroutines e quer esperar que todas terminem, sync.WaitGroup é mais elegante que usar canais. É um contador que você incrementa quando uma goroutine inicia e decrementa quando termina.
package main
import (
"fmt"
"sync"
"time"
)
func processItem(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrementa o contador ao final
fmt.Printf("Processando item %d\n", id)
time.Sleep(500 * time.Millisecond)
fmt.Printf("Item %d completo\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Incrementa o contador
go processItem(i, &wg)
}
wg.Wait() // Bloqueia até o contador chegar a zero
fmt.Println("Todos os itens foram processados")
}
Use WaitGroup quando o trabalho é homogêneo (você não precisa receber diferentes tipos de resultados). Use canais quando precisa de comunicação bidirecional ou quando diferentes goroutines produzem resultados diferentes.
Escalonamento e o Modelo M:N do Runtime
Como Go Agenda Goroutines
Go não usa uma goroutine por thread do SO. Em vez disso, implementa um modelo M:N, onde M goroutines são multiplexadas em N threads do sistema operacional. Internamente, o scheduler do Go mantém uma fila de goroutines prontas e as distribui entre os workers threads. Quando uma goroutine bloqueia em uma operação (como I/O de rede ou arquivo), o scheduler move outra goroutine pronta para aquela thread.
O scheduler do Go é preemptivo apenas em pontos específicos: chamadas para funções da biblioteca padrão, operações de I/O, e alocação de memória. Se uma goroutine executar um loop infinito sem yield, ela pode bloquear a thread. Felizmente, na prática isso é raro porque operações reais (como HTTP requests) causam I/O e liberam a thread.
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func printStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
fmt.Printf("Threads: %d\n", runtime.NumCPU())
fmt.Printf("Memória Alocada: %v MB\n", m.Alloc/1024/1024)
}
func dummyWork(id int, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(1 * time.Second)
}
func main() {
fmt.Println("Estado inicial:")
printStats()
var wg sync.WaitGroup
count := 10000
for i := 0; i < count; i++ {
wg.Add(1)
go dummyWork(i, &wg)
}
fmt.Println("\nCom 10k goroutines:")
printStats()
wg.Wait()
fmt.Println("\nApós conclusão:")
printStats()
}
Execute este código e observe o número de goroutines crescer. Você verá que 10.000 goroutines ocupam poucos MB de RAM — isso demonstra a eficiência do modelo M:N. Cada goroutine consome aproximadamente 2-4 KB de stack inicial.
GOMAXPROCS: Controlando Paralelismo
A variável de ambiente GOMAXPROCS controla quantas threads do SO o Go pode usar simultaneamente. Por padrão, Go utiliza todos os núcleos disponíveis. Você pode também ajustar isso em tempo de execução com runtime.GOMAXPROCS().
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func cpuIntensiveTask(id int, wg *sync.WaitGroup) {
defer wg.Done()
// Simula tarefa que consome CPU
sum := 0
for i := 0; i < 1000000000; i++ {
sum += i
}
fmt.Printf("Task %d completada\n", id)
}
func main() {
// Descobre núcleos disponíveis
numCPU := runtime.NumCPU()
fmt.Printf("CPUs disponíveis: %d\n", numCPU)
// Ajusta GOMAXPROCS para usar apenas 1 núcleo (descomente para testar)
// runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 4; i++ {
wg.Add(1)
go cpuIntensiveTask(i, &wg)
}
wg.Wait()
fmt.Printf("Tempo total: %v\n", time.Since(start))
}
Tarefas CPU-bound (que não bloqueiam) realmente se beneficiam de paralelismo. Se você executar o código acima com GOMAXPROCS(1) e depois com todos os núcleos, verá uma diferença significativa no tempo de execução.
Padrões Avançados e Armadilhas Comuns
Padrão: Fan-Out / Fan-In
Um padrão poderoso é distribuir trabalho para múltiplas goroutines (fan-out) e depois coletar os resultados (fan-in). Isso é útil para paralelizar processamento de uma lista.
package main
import (
"fmt"
"sync"
)
func square(numbers <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for n := range numbers {
results <- n * n
}
}
func main() {
numbers := make(chan int, 5)
results := make(chan int)
// Enche o canal com números
go func() {
for i := 1; i <= 5; i++ {
numbers <- i
}
close(numbers)
}()
// Fan-out: 3 workers processam números em paralelo
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go square(numbers, results, &wg)
}
// Fan-in: coleta resultados
go func() {
wg.Wait()
close(results)
}()
// Consome resultados
for result := range results {
fmt.Println(result)
}
}
Armadilha: Goroutine Leak
Uma goroutine leak ocorre quando uma goroutine é criada mas nunca termina, consumindo recursos indefinidamente. A causa comum é uma goroutine bloqueada em um canal do qual nunca receberá dados.
package main
import (
"fmt"
"time"
)
// ❌ ERRADO: Goroutine que fica bloqueada esperando por sempre
func wrongWay() {
done := make(chan bool)
go func() {
// Esta goroutine fica aqui para sempre esperando
<-done
}()
// main termina sem nunca enviar para done
fmt.Println("Programa terminou, mas goroutine continua!")
}
// ✅ CORRETO: Usar context ou fechar o canal
func rightWay() {
done := make(chan bool, 1)
go func() {
select {
case <-done:
fmt.Println("Goroutine encerrada corretamente")
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}
}()
close(done) // Libera todas as goroutines esperando neste canal
}
func main() {
rightWay()
time.Sleep(2 * time.Second)
}
Padrão: Context para Cancelamento
Para aplicações mais sofisticadas, use context.Context para propagar sinais de cancelamento através de toda a árvore de goroutines.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelado: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d trabalhando...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// Cria context que será cancelado em 2 segundos
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// Espera todas as goroutines terminarem
time.Sleep(3 * time.Second)
fmt.Println("Programa finalizado")
}
Conclusão
Você aprendeu que goroutines são abstrações leves gerenciadas pelo runtime que multiplicam a eficiência da concorrência em Go através do modelo M:N, permitindo criar milhares delas sem o overhead de threads tradicionais. O mecanismo de sincronização principal é o canal, que força você a pensar sobre comunicação entre goroutines de forma segura por padrão — não há race conditions silenciosas como em linguagens que dependem de locks.
O runtime do Go é responsável por agendar essas goroutines nos núcleos disponíveis de forma preemptiva e eficiente, bloqueando quando necessário (I/O) e desbloqueando quando é seguro continuar. Domine canais, WaitGroup e context.Context, e você terá as ferramentas para construir sistemas altamente concorrentes — desde servidores web até processadores de dados paralelos — com código limpo e confiável.