Introdução à concorrência em Go
A linguagem Go (Golang) foi desenvolvida no Google levando em consideração as demandas modernas de desempenho e escalabilidade. Uma das principais características do Go é o suporte embutido para concorrência (concurrency) — a capacidade de um programa executar várias tarefas simultaneamente. Ao contrário do multithreading tradicional, onde as threads são gerenciadas pelo sistema operacional, o Go oferece goroutines leves e primitivas de sincronização convenientes — os canais.
Neste artigo, abordaremos:
- O que são goroutines e como executá-las
- Canais para troca de dados entre goroutines
- Mutexes para acesso seguro a recursos compartilhados
- Exemplos práticos e padrões de código concorrente
1. Goroutines: threads leves do Go
Uma goroutine é uma função que é executada concorrentemente com outras goroutines. Ela ocupa apenas alguns kilobytes de pilha (ao contrário das threads do SO, onde a pilha pode ter megabytes). Para iniciar uma goroutine, basta adicionar a palavra-chave go antes da chamada da função.
Exemplo: Iniciando goroutines
package main
import ( "fmt" "time")
func sayHello(name string) { for i := 0; i < 3; i++ { fmt.Printf("Olá, %s! (iteração %d)", name, i) time.Sleep(100 * time.Millisecond) }}
func main() { go sayHello("Alice") // Iniciando goroutine go sayHello("Bob") // Mais uma goroutine time.Sleep(1 * time.Second) // Aguardando a conclusão fmt.Println("Função principal concluída")}Importante: O programa termina quando a função main termina, mesmo que outras goroutines ainda estejam em execução. Por isso, usamos time.Sleep para aguardar. Em código real, é melhor usar sync.WaitGroup.
Usando sync.WaitGroup
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Decrementa o contador ao finalizar fmt.Printf("Trabalhador %d iniciou o trabalho", id) // ... algum trabalho ... fmt.Printf("Trabalhador %d finalizou o trabalho", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Incrementa o contador antes de iniciar a goroutine go worker(i, &wg) }
wg.Wait() // Bloqueia o main até todas as goroutines terminarem fmt.Println("Todos os trabalhadores finalizaram o trabalho")}2. Canais: troca de dados entre goroutines
Canais (channels) são "tubos" tipados através dos quais goroutines podem enviar e receber valores. Os canais fornecem sincronização segura sem bloqueios explícitos.
Criação e uso de canais
package main
import "fmt"
func main() { // Criando um canal para inteiros ch := make(chan int)
// Iniciando uma goroutine que envia dados go func() { ch <- 42 // Enviando valor para o canal }()
// Recebendo valor do canal value := <-ch fmt.Println("Recebido:", value)}Por padrão, os canais são não bufferizados: o envio é bloqueado até que o receptor esteja pronto para receber os dados, e vice-versa. Isso garante a sincronização.
Canais bufferizados
package main
import "fmt"
func main() { // Canal com buffer para 3 elementos ch := make(chan string, 3)
// Enviando sem bloqueio (enquanto o buffer não estiver cheio) ch <- "um" ch <- "dois" ch <- "três"
// Lendo fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)}Fechamento de canais e range
package main
import "fmt"
func fibonacci(n int, ch chan int) { a, b := 0, 1 for i := 0; i < n; i++ { ch <-