Goroutine
Lição 13: Goroutine
Imagine um restaurante: se houver apenas um garçom (thread única), ele precisa terminar de anotar pedidos, servir comida e cobrar cada mesa antes de ir para a próxima. Contratar muitos garçons (multithreading) seria caro. A solução do Go é contratar um grupo de garçons part-time leves (goroutines) que compartilham o mesmo sistema do restaurante (escalonador) — quem está disponível serve o próximo, lidando com um grande número de clientes com recursos mínimos.
Conceitos Fundamentais
- Uma Goroutine é uma unidade leve de execução concorrente gerenciada pelo runtime do Go, lançada com a palavra-chave
go. - Goroutines não são threads do SO; sua pilha inicial tem apenas cerca de 2KB (threads normalmente requerem 1-8MB), e você pode facilmente criar centenas de milhares delas.
- O runtime do Go usa um modelo de escalonamento M:N (modelo G-M-P): mapeando M goroutines em N threads do SO para execução.
- Goroutines se comunicam via channels (cobertos em detalhes nas lições posteriores) e também podem ser sincronizadas usando o pacote
sync. - Quando a goroutine principal termina, todas as goroutines filhas são encerradas forçadamente sem esperar que terminem.
Sintaxe Básica e Uso
Lançando uma Goroutine
Adicione a palavra-chave go antes de uma chamada de função para lançar uma nova goroutine:
go functionName(args)
// ou
go func() {
// corpo da função anônima
}()
Esperando Goroutines com sync.WaitGroup
Como a goroutine principal não espera automaticamente pelas goroutines filhas, use sync.WaitGroup para sincronização:
var wg sync.WaitGroup
wg.Add(1) // incrementa o contador em 1
go func() {
defer wg.Done() // decrementa o contador ao terminar
// executar tarefa
}()
wg.Wait() // bloqueia até o contador chegar a zero
wg.Add() deve ser chamado antes da instrução go, caso contrário pode ocorrer uma condição de corrida — a goroutine principal pode retornar de Wait antes de Add ser chamado.
wg.Add() dentro da goroutine, caso contrário wg.Wait() pode executar antes de Add.
defer wg.Done() para garantir que o contador decrementa corretamente mesmo se ocorrer um panic.
Exemplos
Exemplo: Lançando Múltiplas Goroutines (Dificuldade ⭐)
package main
import (
"fmt"
"sync"
)
func sayHello(id int, wg *sync.WaitGroup) {
defer wg.Done() // notifica o WaitGroup ao terminar
fmt.Printf("Olá, eu sou a goroutine #%d\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go sayHello(i, &wg)
}
wg.Wait() // espera todas as goroutines terminarem
fmt.Println("Todas as goroutines foram concluídas")
}
A ordem de saída é não determinística (execução concorrente):
Olá, eu sou a goroutine #3
Olá, eu sou a goroutine #1
Olá, eu sou a goroutine #5
Olá, eu sou a goroutine #2
Olá, eu sou a goroutine #4
Todas as goroutines foram concluídas
Exemplo: Computação Concorrente e Agregação de Resultados (Dificuldade ⭐⭐)
package main
import (
"fmt"
"sync"
)
// Computa quadrados concorrentemente e armazena resultados em um slice compartilhado
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
results := make([]int, len(numbers))
var wg sync.WaitGroup
for i, num := range numbers {
wg.Add(1)
go func(index, val int) {
defer wg.Done()
results[index] = val * val // cada goroutine escreve em um índice diferente, sem necessidade de lock
}(i, num) // passa variáveis do loop como argumentos para evitar problemas de captura de closure
}
wg.Wait()
fmt.Println("Dados originais:", numbers)
fmt.Println("Resultados ao quadrado:", results)
}
Saída:
Dados originais: [1 2 3 4 5 6 7 8 9 10]
Resultados ao quadrado: [1 4 9 16 25 36 49 64 81 100]
for cria uma nova cópia a cada iteração, então o passe explícito de parâmetros não é mais necessário. Porém, para compatibilidade e legibilidade, o passe explícito ainda é recomendado.
Exemplo: Pool de Tarefas Concorrente e Prevenção de Vazamento de Goroutines (Dificuldade ⭐⭐⭐)
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)
// task representa uma tarefa que precisa ser processada
func task(ctx context.Context, id int) (string, error) {
// Simula uma operação demorada
duration := time.Duration(rand.Intn(500)) * time.Millisecond
select {
case <-time.After(duration):
return fmt.Sprintf("Tarefa #%d concluída (levou %v)", id, duration), nil
case <-ctx.Done():
return "", ctx.Err() // retorna imediatamente quando o contexto é cancelado, prevenindo vazamento de goroutine
}
}
func main() {
rand.Seed(time.Now().UnixNano())
// Define um timeout total para prevenir vazamento de goroutines
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // garante liberação de recursos
taskCount := 20
results := make(chan string, taskCount) // channel com buffer para coletar resultados
var wg sync.WaitGroup
// Inicia 5 goroutines workers como pool de tarefas
workerCount := 5
for w := 1; w <= workerCount; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for i := workerID; i <= taskCount; i += workerCount {
res, err := task(ctx, i)
if err != nil {
fmt.Printf("Worker %d: Tarefa #%d cancelada\n", workerID, i)
return
}
results <- res
}
}(w)
}
// Fecha o channel após todos os workers terminarem
go func() {
wg.Wait()
close(results)
}()
// Coleta resultados
for res := range results {
fmt.Println(res)
}
fmt.Println("Todas as tarefas processadas")
}
Pontos-chave:
- Usar
context.WithTimeoutcontrola o timeout geral, evitando espera infinita de goroutines. - O padrão de pool de tarefas (número fixo de workers) é mais controlável do que uma goroutine por tarefa.
close(results)fecha o channel após todos os workers terminarem;range resultssai automaticamente quando o channel é fechado.
Casos de Uso Práticos
Caso 1: Requisições HTTP Concorrentes
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
func fetchURL(url string, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("[Erro] %s: %v\n", url, err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
elapsed := time.Since(start)
fmt.Printf("[Concluído] %s — Status: %d, Tamanho: %d bytes, Tempo: %v\n",
url, resp.StatusCode, len(body), elapsed)
}
func main() {
urls := []string{
"https://httpbin.org/get",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/status/200",
}
var wg sync.WaitGroup
start := time.Now()
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg)
}
wg.Wait()
fmt.Printf("\nTudo concluído, tempo total: %v\n", time.Since(start))
// O tempo total é aproximadamente igual à requisição mais lenta, não à soma de todas as requisições
}
Caso 2: Escrita Assíncrona de Logs
package main
import (
"fmt"
"time"
)
type LogEntry struct {
Level string
Message string
}
// logWriter consome logs assincronamente em uma goroutine em segundo plano
func logWriter(entries <-chan LogEntry, done chan<- struct{}) {
for entry := range entries {
// Simula escrita em arquivo ou serviço remoto
time.Sleep(50 * time.Millisecond)
fmt.Printf("[%s] %s\n", entry.Level, entry.Message)
}
done <- struct{}{} // notifica a goroutine principal que a escrita foi concluída
}
func main() {
logCh := make(chan LogEntry, 100) // channel com buffer como fila de logs
done := make(chan struct{})
// Inicia goroutine de escrita de logs em segundo plano
go logWriter(logCh, done)
// Programa principal roda normalmente, logando assincronamente
for i := 1; i <= 5; i++ {
logCh <- LogEntry{
Level: "INFO",
Message: fmt.Sprintf("Processando requisição #%d", i),
}
fmt.Printf("Log #%d enviado\n", i)
}
close(logCh) // fecha o channel para sinalizar ao escritor que deve sair
<-done // espera o escritor terminar
fmt.Println("Programa encerrado")
}
❓ Perguntas Frequentes
1. Por que minha goroutine não está executando?
Quando a goroutine principal termina, ela encerra forçadamente todas as goroutines filhas. Causa comum:
// ❌ Errado: goroutine principal termina imediatamente, goroutine filha não tem chance de executar
func main() {
go fmt.Println("Olá")
}
// ✅ Correto: usa WaitGroup para esperar
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Olá")
}()
wg.Wait()
}
2. Quando goroutines vazam?
Um vazamento de goroutine significa que uma goroutine nunca pode sair, ocupando continuamente memória e recursos do escalonador. Causa comum:
// ❌ Vazamento: channel não tem receptor
func leaky() <-chan int {
ch := make(chan int)
go func() {
ch <- 42 // bloqueado permanentemente, goroutine não pode sair
}()
return ch
}
// ✅ Seguro: usa channel com buffer ou cancelamento de contexto
func safe() <-chan int {
ch := make(chan int, 1) // buffer de 1, envio não bloqueia
go func() {
ch <- 42
}()
return ch
}
3. Como evitar o clássico erro de captura de closure com variáveis de loop?
// ❌ Go 1.21 e anteriores: todas as goroutines podem imprimir o mesmo valor
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // podem todas imprimir 5
}()
}
// ✅ Recomendado: passe explícito de parâmetros, compatível com todas as versões do Go
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n) // imprime corretamente 0, 1, 2, 3, 4
}(i)
}
4. Posso obter o número atual de goroutines em execução?
import "runtime"
fmt.Println("Contagem atual de goroutines:", runtime.NumGoroutine())
Isso é muito útil para depurar vazamentos de goroutines. Um programa ocioso normal tipicamente tem apenas 1-2 goroutines; se o número continuar crescendo, pode haver um vazamento.
📖 Resumo
| Conceito | Descrição |
|---|---|
Palavra-chave go |
Lança uma goroutine, a sintaxe é go func() |
| Modelo de escalonamento | Modelo G-M-P, runtime do Go gerencia o escalonamento, transparente para o usuário |
| Leve | Pilha inicial de cerca de 2KB, pode criar centenas de milhares |
| WaitGroup | Usado para esperar um grupo de goroutines completar |
| Context | Usado para propagar sinais de cancelamento e controle de timeout |
| Prevenção de vazamento | Garanta que cada goroutine tenha um caminho de saída |
Princípios Fundamentais:
- Sempre garanta que goroutines tenham condições de saída claras.
- Use
sync.WaitGroupou channels para sincronizar a goroutine principal com as goroutines filhas. - Use
contextpara controlar ciclos de vida de goroutines e prevenir vazamentos. - Preste atenção na interação entre closures e variáveis de loop (passe explícito de parâmetros é recomendado).
📝 Exercícios
Exercício 1: Verificador de Números Primos Concorrente
Escreva um programa que usa goroutines para verificar concorrentemente se um conjunto de números é primo, depois agregue e exiba os resultados.
// Dicas:
// - Defina uma função isPrime(n int) bool
// - Lance uma goroutine para cada número
// - Use WaitGroup para esperar todas completarem
// - Use mutex ou channel para coletar resultados
Exercício 2: Simulador de Download Concorrente
Simule downloads concorrentes de arquivos: inicie 3 goroutines workers que pegam tarefas de uma fila de tarefas, cada download leva de 100-1000ms aleatório, e reporte o tempo total e o número de tarefas completadas por cada worker.
// Dicas:
// - Use um channel como fila de tarefas
// - Cada worker lê tarefas do channel
// - Use context.WithTimeout para controlar o timeout total
// - Use sync.Map ou mutex para contar as conclusões de cada worker
Exercício 3: Detector de Vazamento de Goroutine
Escreva uma função utilitária que aceita uma função de tarefa como parâmetro, a executa e monitora a contagem de goroutines. Se a contagem de goroutines após a conclusão da tarefa for maior que antes da execução, indica um possível vazamento — exiba um aviso.
// Dicas:
// - Use runtime.NumGoroutine() para obter a contagem de goroutines
// - Registre a contagem antes e depois da execução da tarefa
// - Espere um curto período (ex: 100ms) após a execução antes de verificar
// - Exiba o resultado da detecção



