Concorrência e Multithreading em Go: do A ao Z
Go (Golang) foi projetado pensando nas cargas computacionais modernas. Na era dos processadores multi-core e serviços em nuvem, saber trabalhar eficientemente com concorrência tornou-se uma habilidade obrigatória para desenvolvedores backend. Neste artigo, vamos analisar como o Go resolve problemas de execução paralela de código, como as goroutines diferem das threads do SO e como usar corretamente canais e mutexes.
1. Goroutines: threads leves
Uma goroutine é uma função ou método executado concorrentemente com outras goroutines. Ao contrário das threads do sistema operacional, as goroutines têm sobrecarga mínima: sua pilha começa com 2-4 KB e pode crescer dinamicamente. Iniciar uma goroutine é incrivelmente simples — basta adicionar a palavra-chave go antes da chamada da função.
package main
import ( "fmt" "time")
func say(text string) { for i := 0; i < 3; i++ { fmt.Println(text) time.Sleep(100 * time.Millisecond) }}
func main() { go say("Hello from goroutine") say("Hello from main") // Dá tempo para a goroutine terminar time.Sleep(500 * time.Millisecond)}Observe: o programa termina imediatamente após a execução da função main(), sem esperar a conclusão das goroutines iniciadas. Para sincronização, usamos time.Sleep (em projetos reais, usa-se sync.WaitGroup ou canais).
2. Canais: transferência segura de dados
Canais (channels) são "tubos" tipados através dos quais as goroutines trocam dados. Eles resolvem duas tarefas principais: sincronização de execução e transferência de valores. Os canais podem ser bufferizados ou não bufferizados.
Canal não bufferizado
A operação de envio bloqueia a goroutine até que outra goroutine realize a leitura. Isso garante sincronização "em tempo real".
package main
import "fmt"
func main() { ch := make(chan string)
go func() { ch <- "Olá da goroutine!" }()
msg := <-ch fmt.Println(msg)}Canal bufferizado
Um canal com buffer permite enviar vários valores sem leitura imediata, até que o buffer esteja cheio.
package main
import "fmt"
func main() { ch := make(chan int, 2) ch <- 10 ch <- 20 // ch <- 30 // deadlock! buffer cheio
fmt.Println(<-ch) // 10 fmt.Println(<-ch) // 20}Range e close
Fechar um canal sinaliza aos receptores que não haverá mais dados. O loop for range termina automaticamente quando o canal é fechado.
package main
import "fmt"
func producer(ch chan int) { for i := 1; i <= 5; i++ { ch <- i } close(ch)}
func main() { ch := make(chan int) go producer(ch)
for val := range ch { fmt.Println("Recebido:", val) }}3. Sincronização e mutexes
Apesar do poder dos canais, às vezes é necessário acesso direto a dados compartilhados. O pacote sync fornece mutexes (sync.Mutex) e mutexes de leitura (sync.RWMutex) para proteger seções críticas.
package main
import ( "fmt" "sync")
var counter = 0var mu sync.Mutex
func increment(wg *sync.WaitGroup) { defer wg.Done() mu.Lock() counter++ mu.Unlock()}
func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go increment(&wg) } wg.Wait() fmt.Println("Contador final:", counter)}Aqui, sync.WaitGroup aguarda a conclusão de todas as goroutines, e o mutex previne condições de corrida (data race). Sem o mutex, o resultado seria imprevisível.