Goroutine in Go
Le goroutine sono uno dei concetti fondamentali di Go e rappresentano il meccanismo principale per la programmazione concorrente. Una goroutine è un thread leggero gestito dal runtime di Go, molto piu efficiente di un thread del sistema operativo.
Cosa Sono le Goroutine
Una goroutine è una funzione che viene eseguita in modo concorrente rispetto ad altre goroutine nello stesso spazio di indirizzi. Sono estremamente leggere: una goroutine occupa solo pochi kilobyte di stack (che cresce dinamicamente), mentre un thread del sistema operativo occupa tipicamente 1-2 MB.
Avviare una Goroutine
Per avviare una goroutine si utilizza la parola chiave go seguita dalla chiamata a una funzione:
package main
import (
"fmt"
"time"
)
func saluta(nome string) {
fmt.Println("Ciao,", nome)
}
func main() {
go saluta("Marco") // avvia una goroutine
go saluta("Laura") // avvia un'altra goroutine
time.Sleep(time.Second) // attende per dare tempo alle goroutine
fmt.Println("Fine del programma")
}
La parola chiave go lancia la funzione in una nuova goroutine senza bloccare l’esecuzione della goroutine chiamante.
La Goroutine Main
Ogni programma Go ha almeno una goroutine: la goroutine main, che esegue la funzione main(). Quando la goroutine main termina, tutte le altre goroutine vengono terminate immediatamente, anche se non hanno finito la loro esecuzione:
func main() {
go fmt.Println("Questa potrebbe non essere stampata")
// il programma termina subito, la goroutine potrebbe non fare in tempo
}
Per questo motivo, e fondamentale sincronizzare le goroutine.
Sincronizzazione con WaitGroup
Il pacchetto sync fornisce WaitGroup, lo strumento principale per attendere il completamento di piu goroutine:
package main
import (
"fmt"
"sync"
)
func lavoratore(id int, wg *sync.WaitGroup) {
defer wg.Done() // segnala il completamento
fmt.Printf("Lavoratore %d: inizio\n", id)
// simula del lavoro
fmt.Printf("Lavoratore %d: fine\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // incrementa il contatore
go lavoratore(i, &wg)
}
wg.Wait() // attende che tutte le goroutine completino
fmt.Println("Tutti i lavoratori hanno terminato")
}
I metodi principali di WaitGroup sono:
Add(n): incrementa il contatore dinDone(): decrementa il contatore di 1Wait(): blocca fino a quando il contatore raggiunge 0
Goroutine con Funzioni Anonime
E molto comune avviare goroutine usando funzioni anonime (closure):
func main() {
var wg sync.WaitGroup
messaggi := []string{"ciao", "mondo", "go"}
for _, msg := range messaggi {
wg.Add(1)
go func(m string) {
defer wg.Done()
fmt.Println(m)
}(msg) // passa il valore come argomento
}
wg.Wait()
}
Attenzione: in Go 1.22+, la variabile del ciclo for range viene creata per ogni iterazione, risolvendo un problema storico di cattura della variabile. Tuttavia, e ancora buona pratica passare il valore come argomento alla closure per chiarezza.
Ciclo di Vita di una Goroutine
Una goroutine puo trovarsi in diversi stati:
- Esecuzione: la goroutine sta attivamente eseguendo codice
- In attesa: la goroutine e bloccata (ad esempio in attesa su un canale o un mutex)
- Pronta: la goroutine e pronta per essere eseguita ma attende che lo scheduler le assegni un processore
Lo scheduler di Go utilizza un modello M:N, mappando M goroutine su N thread del sistema operativo, garantendo un uso efficiente delle risorse.
Goroutine vs Thread del Sistema Operativo
| Caratteristica | Goroutine | Thread OS |
|---|---|---|
| Dimensione stack iniziale | ~2-8 KB | ~1-2 MB |
| Creazione | Microsecondi | Millisecondi |
| Gestione | Runtime Go | Sistema operativo |
| Cambio di contesto | Veloce | Lento |
| Numero pratico | Centinaia di migliaia | Migliaia |
Pattern Comuni
Pattern Fan-Out
Distribuire il lavoro su piu goroutine:
func elabora(dati []int) {
var wg sync.WaitGroup
for _, d := range dati {
wg.Add(1)
go func(valore int) {
defer wg.Done()
// elabora il singolo valore
fmt.Println("Elaboro:", valore)
}(d)
}
wg.Wait()
}
Pattern con Timeout
Usare context per gestire il timeout delle goroutine (Go 1.23+):
package main
import (
"context"
"fmt"
"time"
)
func operazioneLunga(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("Operazione completata")
case <-ctx.Done():
fmt.Println("Operazione annullata:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go operazioneLunga(ctx)
time.Sleep(3 * time.Second)
}
Conclusione
Le goroutine sono il cuore della concorrenza in Go. Grazie alla loro leggerezza e semplicita, permettono di scrivere programmi altamente concorrenti senza la complessita tipica della gestione dei thread. Usare sync.WaitGroup per la sincronizzazione e comprendere il ciclo di vita delle goroutine sono competenze essenziali per ogni sviluppatore Go.