Concorrência em Go: goroutines e channels na prática
1. Fundamentos da Concorrência em Go
A linguagem Go foi projetada desde sua origem para lidar com concorrência de forma simples e eficiente. Diferentemente de linguagens tradicionais que dependem de threads do sistema operacional, Go introduziu as goroutines — threads leves gerenciadas pela própria runtime. Uma goroutine consome apenas alguns kilobytes de pilha, permitindo que milhares delas executem simultaneamente sem sobrecarregar o sistema.
É crucial entender a diferença entre concorrência e paralelismo. Concorrência trata de lidar com múltiplas tarefas ao mesmo tempo (estrutura do programa), enquanto paralelismo é sobre executar múltiplas tarefas simultaneamente (execução em múltiplos núcleos). Go suporta ambos, mas a concorrência é o foco principal do modelo.
O que torna Go especial é a simplicidade: criar uma goroutine requer apenas a palavra-chave go antes de uma chamada de função. Combinado com channels para comunicação segura entre goroutines, Go elimina grande parte da complexidade associada à programação concorrente tradicional.
2. Trabalhando com Goroutines
Criar uma goroutine é trivial:
func saudacao(nome string) {
fmt.Printf("Olá, %s!\n", nome)
}
func main() {
go saudacao("Alice")
go saudacao("Bob")
time.Sleep(100 * time.Millisecond) // espera as goroutines terminarem
}
Para sincronização adequada, usamos sync.WaitGroup:
func processar(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Goroutine %d iniciou\n", id)
time.Sleep(time.Second)
fmt.Printf("Goroutine %d finalizou\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go processar(i, &wg)
}
wg.Wait()
fmt.Println("Todas as goroutines terminaram")
}
Goroutines órfãs são um problema comum. Sempre garanta que suas goroutines tenham um mecanismo de finalização claro, seja via WaitGroup, channels ou contextos.
3. Channels: Comunicação entre Goroutines
Channels são os tubos que conectam goroutines. Existem dois tipos:
Unbuffered channels: bloqueiam até que o receptor esteja pronto.
func main() {
ch := make(chan string)
go func() {
ch <- "mensagem da goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
Buffered channels: aceitam um número limitado de valores sem receptor imediato.
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
Para iterar sobre um channel até seu fechamento:
func produzir(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go produzir(ch)
for valor := range ch {
fmt.Println(valor)
}
}
4. Padrões de Concorrência com Channels
Fan-in: combinar múltiplos canais em um.
func fanIn(ch1, ch2 <-chan string) <-chan string {
saida := make(chan string)
go func() {
for {
select {
case msg := <-ch1:
saida <- msg
case msg := <-ch2:
saida <- msg
}
}
}()
return saida
}
Fan-out: distribuir trabalho entre múltiplos consumidores.
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- job * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
}
Pipeline: encadear estágios de processamento.
func gerar(nums ...int) <-chan int {
saida := make(chan int)
go func() {
for _, n := range nums {
saida <- n
}
close(saida)
}()
return saida
}
func quadrado(entrada <-chan int) <-chan int {
saida := make(chan int)
go func() {
for n := range entrada {
saida <- n * n
}
close(saida)
}()
return saida
}
5. Select e Controle de Fluxo
select permite aguardar múltiplas operações de channel simultaneamente:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "um"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "dois"
}()
select {
case msg1 := <-ch1:
fmt.Println("Recebido:", msg1)
case msg2 := <-ch2:
fmt.Println("Recebido:", msg2)
case <-time.After(500 * time.Millisecond):
fmt.Println("Timeout!")
}
}
O default case permite operações não bloqueantes:
select {
case msg := <-ch:
fmt.Println("Recebido:", msg)
default:
fmt.Println("Nenhuma mensagem disponível")
}
6. Sincronização Avançada e Mutexes
Quando múltiplas goroutines acessam dados compartilhados, use sync.Mutex:
type Contador struct {
mu sync.Mutex
valor int
}
func (c *Contador) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock()
c.valor++
}
func (c *Contador) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
Para cenários com muitas leituras e poucas escritas, sync.RWMutex é mais eficiente:
type Cache struct {
mu sync.RWMutex
dados map[string]string
}
func (c *Cache) Ler(chave string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.dados[chave]
}
func (c *Cache) Escrever(chave, valor string) {
c.mu.Lock()
defer c.mu.Unlock()
c.dados[chave] = valor
}
Sempre teste com go run -race para detectar condições de corrida.
7. Contextos e Cancelamento de Goroutines
O pacote context gerencia cancelamento e deadlines:
func operacaoLonga(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := operacaoLonga(ctx)
if err != nil {
fmt.Println("Operação cancelada:", err)
}
}
Sempre passe o contexto como primeiro parâmetro em funções que podem precisar de cancelamento.
8. Padrões de Design e Erros Comuns
Worker pool: reutiliza goroutines para processamento em lote.
func workerPool(tarefas []int, numWorkers int) []int {
tarefasCh := make(chan int, len(tarefas))
resultadosCh := make(chan int, len(tarefas))
for w := 0; w < numWorkers; w++ {
go func() {
for t := range tarefasCh {
resultadosCh <- t * 2
}
}()
}
for _, t := range tarefas {
tarefasCh <- t
}
close(tarefasCh)
var resultados []int
for i := 0; i < len(tarefas); i++ {
resultados = append(resultados, <-resultadosCh)
}
return resultados
}
Erro comum: goroutine leaks. Sempre use defer close(ch) ou cancele contextos.
Para testar concorrência, use o pacote testing com -race e considere usar sync.WaitGroup para sincronização em testes.
Referências
- Documentação oficial de Go: Concorrência — Guia completo sobre goroutines, channels e boas práticas de concorrência em Go.
- Go by Example: Goroutines — Exemplos práticos e interativos de criação e sincronização de goroutines.
- The Go Blog: Share Memory by Communicating — Artigo clássico que explica a filosofia de comunicação entre goroutines via channels.
- YouTube: Concorrência em Go (Full Cycle) — Palestra técnica demonstrando padrões de concorrência com exemplos reais.
- Go Patterns: Concurrency Patterns — Catálogo de padrões de concorrência como fan-in, fan-out e pipelines.
- Documentação do pacote context — Referência completa sobre propagação de cancelamento e deadlines em Go.
- Testing Concurrent Code in Go — Tutorial sobre estratégias para testar código concorrente de forma determinística.