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
go
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
go
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
go
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.