Se existe um recurso que mais distingue Go de outras linguagens modernas, é sua abordagem à concorrência. Goroutines e channels não são uma biblioteca adicionada depois — foram projetados junto com a linguagem, influenciando sua sintaxe, seu runtime e sua filosofia. Compreender concorrência em Go é compreender Go em sua essência.
Este módulo é o mais denso do curso. Cada artigo constrói sobre o anterior, e os conceitos se interconectam. Vale a pena ler com calma, executar os exemplos e experimentar variações.
O problema que concorrência resolve
Programas modernos precisam fazer múltiplas coisas ao mesmo tempo: responder a requisições HTTP enquanto consultam um banco de dados, processar arquivos enquanto enviam notificações, monitorar serviços enquanto coletam métricas. Sem concorrência, essas tarefas aconteceriam sequencialmente — uma esperando a outra terminar.
Há duas abordagens clássicas para concorrência: threads do sistema operacional e I/O assíncrono com callbacks. Threads são poderosas mas caras — cada thread consome tipicamente 1 a 8 MB de memória de pilha e tem custo significativo de criação e troca de contexto. Callbacks resolvem o problema de memória mas produzem código difícil de ler e debugar — o famoso "callback hell".
Go oferece uma terceira via: goroutines.
O que é uma goroutine
Uma goroutine é uma função executada de forma concorrente com outras goroutines no mesmo processo. Ela é gerenciada pelo runtime do Go, não diretamente pelo sistema operacional.
A diferença prática é radical:
- Uma thread do SO ocupa entre 1 MB e 8 MB de pilha
- Uma goroutine começa com apenas 2 KB de pilha, que cresce e encolhe dinamicamente conforme necessário
- Criar uma goroutine é ordens de magnitude mais barato do que criar uma thread
- Um programa Go típico pode ter dezenas de milhares de goroutines simultâneas sem problemas
O runtime do Go usa um scheduler M:N — ele mapeia M goroutines em N threads do sistema operacional, onde N é tipicamente igual ao número de núcleos disponíveis. O scheduler é cooperativo e preemptivo: ele alterna goroutines em pontos de I/O, chamadas de sistema e periodicamente em loops longos.
Criando goroutines
A sintaxe é deliberadamente minimalista — a palavra-chave go antes de qualquer chamada de função:
package main
import (
"fmt"
"time"
)
func saudar(nome string) {
fmt.Printf("Olá, %s!\n", nome)
}
func main() {
go saudar("Ana") // executa em uma goroutine separada
go saudar("Bruno")
go saudar("Carla")
// Sem isso, main termina antes das goroutines executarem
time.Sleep(100 * time.Millisecond)
fmt.Println("main finalizada")
}
O time.Sleep no final é uma muleta — existe para dar tempo às goroutines de executar antes que main termine. Quando main retorna, todas as goroutines são encerradas abruptamente, independentemente de terem terminado ou não. A solução correta para coordenar goroutines será o tema dos próximos artigos.
Goroutines com funções anônimas
O padrão mais comum é combinar go com funções anônimas:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
i := i // captura o valor atual — essencial antes do Go 1.22
go func() {
defer wg.Done()
fmt.Printf("goroutine %d executando\n", i)
}()
}
wg.Wait() // bloqueia até todas as goroutines terminarem
fmt.Println("todas as goroutines finalizadas")
}
sync.WaitGroup é a solução idiomática para esperar que um grupo de goroutines termine. Será detalhado no Artigo 22 — por ora, entenda que wg.Add(1) incrementa um contador, wg.Done() o decrementa e wg.Wait() bloqueia até o contador chegar a zero.
O modelo de memória do Go
Go segue o princípio "Do not communicate by sharing memory; instead, share memory by communicating" — não comunique por memória compartilhada; compartilhe memória comunicando.
Na prática, isso significa que a forma preferida de passar dados entre goroutines é através de channels — não por variáveis compartilhadas com locks. Isso não significa que variáveis compartilhadas com sincronização são proibidas — são legítimas e às vezes mais eficientes — mas a filosofia do Go favorece o modelo de channels.
Concorrência vs paralelismo
Uma distinção fundamental que frequentemente gera confusão:
Concorrência é sobre estrutura — um programa que pode lidar com múltiplas tarefas ao mesmo tempo, alternando entre elas conforme necessário. Mesmo em um único núcleo, um programa concorrente pode interleavar goroutines.
Paralelismo é sobre execução simultânea — múltiplas tarefas executando literalmente ao mesmo tempo em múltiplos núcleos.
Go oferece ambos. A variável GOMAXPROCS controla quantas threads do sistema operacional o runtime usa simultaneamente:
package main
import (
"fmt"
"runtime"
)
func main() {
// Por padrão, usa todos os núcleos disponíveis
fmt.Println("Núcleos:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
// Forçar execução em apenas 1 thread (sem paralelismo real)
runtime.GOMAXPROCS(1)
// Ceder voluntariamente o processador para outras goroutines
runtime.Gosched()
}
Desde o Go 1.5, GOMAXPROCS é igual ao número de CPUs por padrão. Alterar esse valor raramente é necessário em código de aplicação.
Um exemplo real: requisições HTTP concorrentes
O poder das goroutines se manifesta em cenários práticos. Comparando execução sequencial e concorrente de múltiplas requisições HTTP:
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func verificarURL(url string, wg *sync.WaitGroup, resultados chan<- string) {
defer wg.Done()
inicio := time.Now()
resp, err := http.Get(url)
duracao := time.Since(inicio)
if err != nil {
resultados <- fmt.Sprintf("ERRO %-35s %v", url, err)
return
}
defer resp.Body.Close()
resultados <- fmt.Sprintf("%-5d %-35s %v", resp.StatusCode, url, duracao)
}
func main() {
urls := []string{
"https://go.dev",
"https://pkg.go.dev",
"https://github.com",
"https://google.com",
"https://cloudflare.com",
}
var wg sync.WaitGroup
resultados := make(chan string, len(urls))
inicio := time.Now()
for _, url := range urls {
wg.Add(1)
go verificarURL(url, &wg, resultados)
}
// Goroutine separada para fechar o channel após todas terminarem
go func() {
wg.Wait()
close(resultados)
}()
for resultado := range resultados {
fmt.Println(resultado)
}
fmt.Printf("\nTotal: %v\n", time.Since(inicio))
}
Executando sequencialmente, 5 requisições que demoram 300 ms cada totalizariam 1,5 segundo. Executando concorrentemente, todas acontecem em paralelo e o total se aproxima do tempo da requisição mais lenta — cerca de 300 ms.
Goroutines e o scheduler: pontos de troca
O scheduler do Go troca goroutines em pontos específicos. Entender esses pontos ajuda a escrever código mais previsível:
- Operações de I/O — leitura/escrita em arquivos, rede, pipes
- Chamadas de sistema
- Operações em channels — envio e recebimento
time.Sleepruntime.Gosched()— cessão voluntária- Alocações de memória em certas condições
- Chamadas de função em loops (preempção assíncrona desde Go 1.14)
Antes do Go 1.14, um loop CPU-bound sem pontos de troca podia monopolizar uma thread e impedir outras goroutines de executar. A preempção assíncrona resolveu esse problema — o scheduler pode interromper goroutines mesmo em loops puros.
Goroutines vazando: um problema real
Uma goroutine leak acontece quando uma goroutine é criada mas nunca termina — ficando presa em um channel bloqueado, em um loop infinito ou esperando por um recurso que nunca chegará. Com o tempo, goroutines acumuladas consomem memória e degradam a performance.
// PROBLEMA: goroutine vaza se ninguém lê do channel
func vazamento() {
ch := make(chan int)
go func() {
ch <- 42 // bloqueia para sempre se ninguém receber
}()
// função retorna sem ler do channel — goroutine fica presa
}
// SOLUÇÃO: usar context para cancelamento (detalhado no Artigo 23)
func semVazamento(ctx context.Context) {
ch := make(chan int, 1) // channel com buffer
go func() {
select {
case ch <- 42:
case <-ctx.Done(): // goroutine termina se o contexto for cancelado
}
}()
}
Para detectar goroutine leaks em testes, o pacote goleak da Uber é amplamente usado:
go get go.uber.org/goleak
func TestSemVazamento(t *testing.T) {
defer goleak.VerifyNone(t)
// ... código do teste
}
Inspecionando goroutines em runtime
O pacote runtime permite inspecionar goroutines em execução:
package main
import (
"fmt"
"runtime"
"time"
)
func trabalho(id int) {
time.Sleep(1 * time.Second)
}
func main() {
fmt.Printf("Goroutines antes: %d\n", runtime.NumGoroutine())
for i := 0; i < 10; i++ {
go trabalho(i)
}
fmt.Printf("Goroutines durante: %d\n", runtime.NumGoroutine())
time.Sleep(2 * time.Second)
fmt.Printf("Goroutines após: %d\n", runtime.NumGoroutine())
}
Em produção, expor métricas de goroutines é uma prática recomendada — um número crescente e irrestrito de goroutines é um sinal claro de vazamento.
Stack traces de goroutines
Quando um programa Go entra em pânico, o runtime imprime o stack trace de todas as goroutines ativas — não apenas a que causou o problema. Isso é extremamente útil para diagnóstico:
package main
import "sync"
func deadlock() {
var mu sync.Mutex
mu.Lock()
mu.Lock() // deadlock — trava esperando por si mesmo
}
func main() {
deadlock()
}
A saída de pânico mostrará todas as goroutines, seus estados e onde cada uma está bloqueada. Para obter o stack trace de todas as goroutines a qualquer momento sem pânico, use runtime/debug:
import "runtime/debug"
debug.PrintStack() // imprime stack da goroutine atual
Ou envie o sinal SIGQUIT ao processo — Go imprime o stack de todas as goroutines e continua executando.
Resumo do que foi coberto
Este artigo apresentou goroutines em profundidade: o que são, como diferem de threads tradicionais, o modelo de scheduler M:N, a sintaxe de criação com go, a distinção entre concorrência e paralelismo, GOMAXPROCS, um exemplo prático de requisições concorrentes, os pontos de troca do scheduler, goroutine leaks e como evitá-las, e inspeção de goroutines em runtime. O próximo artigo apresenta channels — o mecanismo de comunicação entre goroutines.
Referências e leituras complementares
-
Go Blog: Concurrency is not Parallelism — Rob Pike sobre a distinção fundamental. https://go.dev/blog/waza-talk
-
Go Blog: Go Concurrency Patterns — Padrões clássicos de concorrência em Go. https://go.dev/blog/pipelines
-
Documentação do pacote runtime — Referência de NumGoroutine, GOMAXPROCS e Gosched. https://pkg.go.dev/runtime
-
Go by Example: Goroutines — Exemplos práticos comentados. https://gobyexample.com/goroutines
-
goleak — Uber's goroutine leak detector — Ferramenta para detectar vazamentos em testes. https://github.com/uber-go/goleak
-
The Go Memory Model — Especificação formal de como goroutines enxergam memória compartilhada. https://go.dev/ref/mem