Salta al contenuto
Tech7 gen 2026

Concurrency in Go: 3 pattern per codice efficiente e sicuro

Worker Pool, Broadcast Stop Signal, Fan-In. I 3 pattern di concorrenza in go che rendono il codice effiicente, sicuro e manutenibile.

Di Paolo Galeotti

Go è famoso per le sue funzionalità di concorrenza integrate come goroutine e channel. Ma usarle efficacemente richiede più di un semplice go func(): serve struttura. In questo articolo, esploreremo tre pattern di concorrenza pratici che ti aiuteranno a costruire programmi concorrenti affidabili e manutenibili in Go.

Indice dei contenuti:

  • Worker Pool – limitare il numero di goroutine che processano job
  • Broadcast Stop Signal – fermare più goroutine in modo pulito
  • Fan-In – combinare output da più goroutine

Ogni pattern include esempi di codice, linee guida per l'uso e casi d'uso reali.

1. Worker Pool: concorrenza controllata

Immagina di avere una lista di task da eseguire, magari ridimensionare immagini, fare scraping di URL, o inviare email. Vuoi processarne molti in parallelo, ma senza generare centinaia di goroutine (che potrebbero esaurire le risorse di sistema o sovraccaricare servizi esterni).

Un Worker Pool risolve questo problema avviando un numero fisso di goroutine (i "worker") e alimentandole con task attraverso un channel. Ogni worker prende un task, lo processa, poi aspetta il successivo.

In questo modo, mantieni la concorrenza sotto controllo e il tuo programma diventa più prevedibile ed efficiente.

🔧 Esempio di codice

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(time.Second) // Lavoro simulato
        results <- job * 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Avvia 3 worker
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Invia i job
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Raccoglie i risultati
    for a := 1; a <= numJobs; a++ {
        fmt.Println("Result:", <-results)
    }
}

✅ Quando usarlo

  • Hai molti task piccoli e indipendenti
  • Vuoi limitare la concorrenza (per evitare di sovraccaricare un server)
  • I tuoi worker possono essere riutilizzati e non necessitano di stato unico persistente

❌ Quando evitarlo

  • I task sono infrequenti: creare goroutine ad-hoc potrebbe essere più semplice
  • Ogni worker ha bisogno del proprio stato long-lived (come una connessione DB)
  • Hai bisogno di parallelismo istantaneo senza limiti (usa goroutine direttamente, con attenzione)

💡 Casi d'uso reali

  • Invio di email in batch
  • Chiamate API con rate limits
  • Processamento di job da una coda

2. Broadcast Stop Signal: shutdown coordinato

A volte hai più goroutine in esecuzione in background, magari in ascolto di eventi o processando job. Quando la tua app si spegne (o si verifica un errore), vuoi segnalare a tutte di fermarsi.

Questo pattern è semplice ed evita errori comuni come usare context.Context in struct long-lived (che è considerato un anti-pattern).

🔧 Esempio di codice

type Worker struct {
    id     int
    stopCh <-chan struct{}
}

func (w Worker) Start() {
    go func() {
        for {
            select {
            case <-w.stopCh:
                fmt.Printf("Worker %d stopping\n", w.id)
                return
            default:
                fmt.Printf("Worker %d is working...\n", w.id)
                time.Sleep(500 * time.Millisecond)
            }
        }
    }()
}

func main() {
    stop := make(chan struct{})
    workers := []Worker{
        {1, stop},
        {2, stop},
    }

    for _, w := range workers {
        w.Start()
    }

    time.Sleep(2 * time.Second)
    close(stop) // Segnale di stop broadcast
    time.Sleep(1 * time.Second) // Aspetta che i worker si fermino
}

Questo funziona perché in Go quando chiudi un channel viene segnalato a tutti i consumer. Non può essere fatto semplicemente inviando dati sul channel perché questo verrebbe consumato solo dalla prima goroutine disponibile e non dalle altre.

✅ Quando usarlo

  • Hai goroutine long-running (come listener o poller)
  • Vuoi un segnale di shutdown centralizzato
  • Non hai bisogno di timeout per operazione o propagazione di errori

❌ Quando evitarlo

  • Per logica scoped alla richiesta: preferisci context.Context
  • Quando usi timeout, deadline, o catene di cancellazione parent-child
  • Quando ogni goroutine gestisce il proprio ciclo di vita separatamente

⚠️ Perché non usare context.Context qui?

Usare context.Context come campo in una struct long-lived è un anti-pattern perché:

  • I context sono pensati per essere passati, non memorizzati
  • Rappresentano il lifetime di un'operazione, non di un componente
  • Riutilizzare un context cancellato porta a bug sottili e comportamenti indefiniti

Invece, usa un semplice channel come stopCh per segnalare lo shutdown del componente: è idiomatico e più sicuro.

💡 Casi d'uso reali

  • Fermare gracefully worker in background allo shutdown dell'app
  • Fermare consumer da un message broker (es. Kafka)
  • Interrompere listener TCP/UDP su SIGTERM

3. Fan-In: aggregazione di stream

A volte hai più goroutine che producono dati indipendentemente (scraper, sensori, lettori di file, ecc.), e vuoi combinare i loro output in un singolo stream.

Il pattern Fan-In unisce più channel di input in un channel di output. Questo rende facile centralizzare la logica di processing mantenendo i producer separati e indipendenti.

🔧 Esempio di codice

func producer(id int, out chan<- string) {
    for i := 0; i < 3; i++ {
        out <- fmt.Sprintf("Producer %d: item %d", id, i)
        time.Sleep(time.Duration(id) * 200 * time.Millisecond)
    }
    close(out)
}

func fanIn(channels ...<-chan string) <-chan string {
    out := make(chan string)
    for _, ch := range channels {
        go func(c <-chan string) {
            for val := range c {
                out <- val
            }
        }(ch)
    }
    return out
}

func main() {
    a := make(chan string)
    b := make(chan string)

    go producer(1, a)
    go producer(2, b)

    merged := fanIn(a, b)

    for i := 0; i < 6; i++ {
        fmt.Println("Received:", <-merged)
    }
}

✅ Quando usarlo

  • Più goroutine producono valori concorrentemente
  • Vuoi unire i loro output per processarli in un posto
  • Ogni producer può funzionare indipendentemente

❌ Quando evitarlo

  • Se l'ordine dell'output è importante (fan-in non lo preserva)
  • Se hai bisogno di tracciare da quale sorgente viene ogni messaggio
  • Se i producer non stanno funzionando concorrentemente: un semplice merge potrebbe bastare

💡 Casi d'uso reali

  • Unire log da più microservizi
  • Combinare risultati da scraper HTTP paralleli
  • Aggregare eventi da diverse sorgenti di input

Conclusione

Ognuno di questi pattern di concorrenza aiuta a strutturare il tuo codice Go per chiarezza, sicurezza e performance.

Comprendendo quando usare questi pattern, e quando non usarli, puoi costruire sistemi concorrenti in Go che sono veloci, sicuri e manutenibili.

Questo articolo è stato scritto da Paolo Galeotti, Backend Developer di Quinck. Leggi altri articoli sul nostro blog per approfondimenti su sviluppo software e innovazione tecnologica.

SERVIZIO COLLEGATOSoftware su misura

Ti abbiamo incuriosito?

Se avete un problema, un'idea o solo curiosità: parliamone. Mezz'ora, senza impegno.

Scrivici

Articoli correlati