00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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 di n
  • Done(): decrementa il contatore di 1
  • Wait(): 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:

  1. Esecuzione: la goroutine sta attivamente eseguendo codice
  2. In attesa: la goroutine e bloccata (ad esempio in attesa su un canale o un mutex)
  3. 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.