Concorrência e Multithreading em Go: de goroutines a canais
A linguagem Go (Golang) foi desenvolvida no Google pensando em requisitos modernos de desempenho e escalabilidade. Sua principal "característica" é o suporte embutido à concorrência, implementado no nível da linguagem, não através de bibliotecas externas. Diferente do Python, onde o multithreading é limitado pelo GIL (Global Interpreter Lock), o Go oferece goroutines leves que permitem executar milhares de tarefas simultaneamente de forma eficiente. Neste artigo, vamos detalhar os conceitos-chave de concorrência e multithreading em Go, incluindo goroutines, canais, mutexes e grupos de espera, com exemplos práticos de código.
1. Goroutines: threads leves em Go
Uma goroutine é uma função que executa concorrentemente com outras funções no mesmo espaço de endereçamento. Ela roda sobre threads do sistema, mas usa o próprio escalonador do Go, tornando-se significativamente mais barata que uma thread comum do SO. Uma goroutine é criada usando a palavra-chave go.
package main
import ( "fmt" "time")
func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}
func main() { go say("Hello") // Iniciando uma goroutine say("World") // Chamada normal na thread principal}Neste exemplo, say("Hello") executa em uma goroutine separada, enquanto say("World") executa na principal. A saída será intercalada porque ambas as funções trabalham concorrentemente. É importante lembrar: se a função main terminar, todas as goroutines serão forçadas a parar.
2. Canais: transferência segura de dados entre goroutines
Canais são "tubos" tipados através dos quais as goroutines se comunicam entre si. Eles fornecem sincronização sem usar mutexes explicitamente. Canais podem ser com buffer (com capacidade) e sem buffer (síncronos).
Canal sem buffer
package main
import "fmt"
func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // enviando o resultado para o canal}
func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int)
go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
x, y := <-c, <-c // recebendo do canal
fmt.Println(x, y, x+y)}Aqui, duas goroutines calculam as somas das metades do array e enviam os resultados para o canal. A goroutine principal bloqueia até receber ambos os valores. Um canal sem buffer atua como um ponto de sincronização: o envio bloqueia até que o receptor esteja pronto, e vice-versa.
Canal com buffer
package main
import "fmt"
func main() { ch := make(chan int, 2) // buffer para 2 elementos ch <- 1 ch <- 2 // ch <- 3 // isso causará um deadlock porque o buffer está cheio fmt.Println(<-ch) fmt.Println(<-ch)}Um canal com buffer permite enviar dados sem bloquear até que o buffer esteja cheio. Isso é conveniente quando a velocidade de envio é maior que a velocidade de processamento.
3. Sincronização: WaitGroup e mutexes
Embora os canais cubram a maioria dos cenários, às vezes é necessário um controle mais fino da sincronização. Para isso, o pacote sync fornece WaitGroup (espera por um grupo de goroutines terminarem) e Mutex (exclusão mútua para proteger dados compartilhados).
WaitGroup: esperando todas as goroutines
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // decrementa o contador ao finalizar fmt.Printf("Worker %d starting", id) // simulando trabalho fmt.Printf("Worker %d done", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // incrementa