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


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
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:

GO
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
💡 Dica: 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.

💡 Dica: Não coloque wg.Add() dentro da goroutine, caso contrário wg.Wait() pode executar antes de Add.

💡 Dica: Sempre use defer wg.Done() para garantir que o contador decrementa corretamente mesmo se ocorrer um panic.


Exemplos

Exemplo: Lançando Múltiplas Goroutines (Dificuldade ⭐)

GO
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")
}
▶ Experimente

A ordem de saída é não determinística (execução concorrente):

TEXT
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 ⭐⭐)

GO
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)
}
▶ Experimente

Saída:

TEXT
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]
💡 Dica: A partir do Go 1.22, a variável do loop 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 ⭐⭐⭐)

GO
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")
}
▶ Experimente

Pontos-chave:


Casos de Uso Práticos

Caso 1: Requisições HTTP Concorrentes

GO
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

GO
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:

GO
// ❌ 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:

GO
// ❌ 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
// ❌ 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?

GO
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:

  1. Sempre garanta que goroutines tenham condições de saída claras.
  2. Use sync.WaitGroup ou channels para sincronizar a goroutine principal com as goroutines filhas.
  3. Use context para controlar ciclos de vida de goroutines e prevenir vazamentos.
  4. 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.

GO
// 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.

GO
// 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.

GO
// 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

Próxima Lição: Channel →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%