Entendendo Goroutines, Concorrência e Paralelismo em Go
O git-lrc é um revisor de código com IA que analisa alterações automaticamente a cada commit, ajudando a identificar bugs e mudanças inesperadas. O projeto é gratuito, ilimitado e open source, permitindo que qualquer desenvolvedor utilize e contribua. Goroutines são funções leves do Go que permitem executar tarefas de forma concorrente e, em CPUs com múltiplos núcleos, paralela. O Go utiliza um scheduler eficiente baseado no modelo G-M-P, permitindo executar milhares de goroutines com poucas threads do sistema operacional.
Estou desenvolvendo o git-lrc, um revisor de código com IA que é executado a cada commit. É gratuito, ilimitado e o código-fonte está disponível no Github. Dê uma estrela no projeto para ajudar outros desenvolvedores a descobri-lo. Experimente e compartilhe seu feedback para que possamos aprimorá-lo.
Ao começar a usar Go, uma das coisas mais empolgantes que você encontrará são as goroutines , uma maneira simples, porém incrivelmente poderosa, de lidar com concorrência e paralelismo.
Mas o que são exatamente goroutines? Como elas funcionam? E qual a diferença real entre concorrência e paralelismo quando sua máquina possui múltiplos núcleos de CPU?
Vamos analisar tudo isso com código e conhecimento profundo.
O que são Goroutines?
Uma goroutine é uma função leve, de execução independente, gerenciada pelo ambiente de execução do Go.
Você inicia uma com a gopalavra-chave:
go myFunction() Essa linha diz "Go":
“Executar
myFunction()em segundo plano — não bloquear a função atual.”
Ao contrário das threads do sistema operacional, as goroutines são extremamente baratas — você pode criar milhares ou até milhões delas.
Cada uma começa com uma pequena pilha (cerca de 2 KB) que cresce dinamicamente, diferentemente das pilhas de threads de tamanho fixo.
Como as Goroutines Funcionam Internamente
Go utiliza um agendador M:N , o que significa:
- M = número de threads do sistema operacional
- N = número de goroutines
O ambiente de execução mapeia muitas goroutines (N) em um conjunto menor de threads do sistema operacional (M) e as distribui entre os núcleos da CPU.
Ele gerencia a troca de contexto, o agendamento e a sincronização nos bastidores, sem a necessidade de lidar manualmente com pthread_create()pools de threads.
Concorrência versus Paralelismo
Esses dois conceitos são frequentemente confundidos, mas são ideias diferentes.
| Prazo | Significado | Analogia |
|---|---|---|
| Concorrência | Várias tarefas em andamento (não necessariamente simultâneas) | Preparar o jantar enquanto checa o celular — alternando rapidamente entre as opções. |
| Paralelismo | Várias tarefas sendo executadas literalmente ao mesmo tempo | Você e um amigo cozinhando pratos diferentes em fogões separados. |
As goroutines oferecem concorrência por padrão. O paralelismo
só ocorre quando vários núcleos executam goroutines simultaneamente.
Exemplo 1: Concorrência (Núcleo Único)
Eis como a concorrência se comporta quando o Go está limitado a um único núcleo de CPU:
package main import ( "fmt" "runtime" "time" ) func worker(id int) { for i := 1; i <= 3; i++ { fmt.Printf("Worker %d running iteration %d\n", id, i) time.Sleep(400 * time.Millisecond) } } func main() { runtime.GOMAXPROCS(1) // use only 1 OS thread fmt.Println("Using GOMAXPROCS:", runtime.GOMAXPROCS(0)) fmt.Println("---- Concurrency Demo ----") go worker(1) go worker(2) go worker(3) time.Sleep(3 * time.Second) } Embora as três goroutines estejam ativas, apenas um núcleo da CPU as executa em um dado momento.
O agendador alterna rapidamente entre elas, criando a ilusão de trabalho simultâneo.
Isso é concorrência — sobreposição no tempo, não na execução.
Exemplo 2: Paralelismo (Multi-Core)
Agora vamos ver o verdadeiro paralelismo permitindo que o Go utilize todos os núcleos de CPU disponíveis:
package main import ( "fmt" "runtime" "sync" "time" ) func heavyWork(id int, wg *sync.WaitGroup) { defer wg.Done() start := time.Now() sum := 0 for i := 0; i < 5e7; i++ { sum += i } fmt.Printf("Worker %d done in %v (sum=%d)\n", id, time.Since(start), sum) } func main() { cores := runtime.NumCPU() runtime.GOMAXPROCS(cores) fmt.Printf("Detected %d cores\n", cores) fmt.Println("---- Parallelism Demo ----") var wg sync.WaitGroup for i := 1; i <= cores; i++ { wg.Add(1) go heavyWork(i, &wg) } wg.Wait() fmt.Println("All workers done.") } Cada goroutine executa tarefas que exigem muito da CPU.
O agendador do Go as distribui por todos os núcleos da CPU, executando-as em verdadeiro paralelo.
Em uma CPU de 8 núcleos, o tempo total de execução será aproximadamente o tempo de uma única heavyWork()execução — não oito vezes maior.
Isso é paralelismo de verdade .
Como o Go utiliza seus núcleos
Você pode inspecionar e controlar quantos núcleos o Go utiliza:
fmt.Println(runtime.NumCPU()) // total logical CPUs fmt.Println(runtime.GOMAXPROCS(0)) // CPUs Go is currently using Desde o Go 1.5 , o Go utiliza automaticamente todas as CPUs lógicas disponíveis por padrão.
Você pode sobrescrever esse comportamento usando:
runtime.GOMAXPROCS(n) Por exemplo, configurar GOMAXPROCS(2)um sistema de 8 núcleos significa que o Go irá agendar as goroutines em apenas 2 núcleos.
Cargas de trabalho limitadas pela CPU versus cargas de trabalho limitadas por E/S
Quando é que núcleos extras realmente ajudam? Depende da sua carga de trabalho:
| Tipo | Descrição | Exemplos | Benefícios de múltiplos núcleos |
|---|---|---|---|
| limitado pela CPU | Computação pesada | matemática, compressão, processamento de imagens | ✅ Aumento significativo de velocidade |
| Ligado a E/S | Aguardando E/S de rede/disco | Chamadas de API, consultas de banco de dados, entrada/saída de arquivos | ⚙️ Limitado (principalmente concorrência) |
| Misturado | Computação + E/S | API + pós-processamento | ⚡ Bom |
Portanto, mais núcleos só são úteis quando suas goroutines estão realmente executando tarefas na CPU, e não apenas aguardando.
Aguardando a conclusão das Goroutines
Por padrão, a main()função termina imediatamente, encerrando quaisquer goroutines em execução.
Use sync.WaitGrouppara aguardar até que tudo esteja concluído:
var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) go func(id int) { defer wg.Done() fmt.Println("Worker", id, "done") }(i) } wg.Wait() // blocks until all goroutines complete Como funciona o Go Scheduler (Análise Detalhada)
O agendador de tempo de execução do Go gira em torno de três componentes:
| Prazo | Significado |
|---|---|
| G | Goroutine (sua tarefa leve) |
| M | Máquina (um thread do SO) |
| P | Processador (um contexto lógico que agenda Gs em Ms) |
Sempre há GOMAXPROCSum número de Ps.
Cada P gerencia um M e executa vários Gs usando uma fila local.
Se um P ficar sem trabalho, ele " rouba " goroutines de outro — isso é roubo de trabalho , mantendo as CPUs ocupadas de forma eficiente.
É assim que o Go consegue lidar com milhões de goroutines em poucas threads do sistema operacional.
Conclusão
Goroutines são o superpoder do Go, tornando a programação concorrente e paralela simples e eficiente.
- Utilize goroutines para concorrência (várias tarefas "em andamento").
- Utilizado
GOMAXPROCS(runtime.NumCPU())para paralelismo (todos os núcleos ativos) - Combine ambos para obter escalabilidade massiva.
Mesmo com milhões de goroutines, o Go mantém-se eficiente graças ao seu agendador de roubo de trabalho , dimensionamento dinâmico da pilha e modelo de threading M:N .

*Os agentes de IA escrevem código rapidamente. Eles também removem lógica silenciosamente, alteram comportamentos e introduzem bugs — sem que você perceba. Muitas vezes, você só descobre isso em produção.
O git-lrc resolve isso. Ele se integra ao comando `git commit` e revisa cada diferença antes que ela seja aplicada. Configuração em 60 segundos. Completamente gratuito.*
Qualquer feedback ou contribuição é bem-vindo! Está online, o código-fonte está disponível e pronto para qualquer pessoa usar.


Comentários
Ainda não há comentários. Seja o primeiro.
Escreva um comentário