f in x
Goroutine e channel in Go: concorrenza nativa senza librerie esterne
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Sviluppo di siti web

Goroutine e channel in Go: concorrenza nativa senza librerie esterne

[2026-06-10] Author: Ing. Calogero Bono

Il tuo backend Go gestisce le richieste una dopo l’altra? Stai usando solo il 20% del motore. La concorrenza nativa di Go — goroutine e channel — è il suo superpotere più sottovalutato. Non servono librerie, non serve una coda esterna. Serve capire come far parlare i pezzi tra loro senza fare casino.

Noi, di Meteora Web, abbiamo scelto Go per alcuni progetti backend proprio per questa ragione: scrivere codice concorrente pulito, senza framework di messaggistica. In questa guida ti portiamo dentro il modello CSP (Communicating Sequential Processes) di Go: goroutine come attori leggeri, channel come pipeline. Pronto a smettere di usare Go come se fosse PHP?

Goroutine: il thread costa poco, lanciarlo costa pochissimo

Una goroutine è un thread leggero gestito dal runtime Go. Costa circa 4KB di stack iniziale (contro il MB di un thread OS). Puoi avviarne centinaia di migliaia senza impazzire.

package main

import (
	"fmt"
	"time"
)

func stampa(msg string) {
	for i := 0; i < 3; i++ {
		fmt.Println(msg, i)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	go stampa("goroutine") // lanciata in background
	stampa("main")         // eseguita sul goroutine principale
	time.Sleep(1 * time.Second)
}

Problema: non aspetti la fine. Se il main termina, tutte le goroutine muoiono. Servono i WaitGroup.

Sponsored Protocol

sync.WaitGroup: aspetta che finiscano

var wg sync.WaitGroup

func lavoro(id int) {
	defer wg.Done()
	fmt.Printf("Lavoro %d iniziato\n", id)
	time.Sleep(200 * time.Millisecond)
	fmt.Printf("Lavoro %d finito\n", id)
}

func main() {
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go lavoro(i)
	}
	wg.Wait()
	fmt.Println("Tutti finiti")
}

Errore comune: chiamare wg.Add dentro la goroutine invece che fuori. Fallo sempre prima del go, o usalo noto all’avvio. Noi lo vediamo spesso in code review: deadlock sicuro.

Channel: tubi che trasportano dati tra goroutine

Un channel è un canale tipizzato (chan Tipo) che blocca in lettura finché non arriva un dato, e blocca in scrittura finché il buffer non si libera (se bufferizzato) o finché qualcuno non legge (se unbuffered).

ch := make(chan string)    // unbuffered
chBuff := make(chan int, 5) // bufferizzato, capacità 5

Unbuffered: sincrono puro

Un produttore scrive → si blocca finché un consumatore legge. Perfetto per notifiche e sincronizzazione.

func main() {
	ch := make(chan string)

	go func() {
		ch <- "Ciao da goroutine"
	}()

	msg := <-ch
	fmt.Println(msg)
}

Se scrivi senza mai leggere — deadlock. Il runtime Go se ne accorge e crasha.

Sponsored Protocol

Buffered: coda interna

Scrivi fino a esaurire buffer, poi si blocca. Utile per carichi di burst.

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // bloccante: buffer pieno
fmt.Println(<-ch) // 1

Quando usarlo? Quando il produttore è più veloce del consumatore per brevi periodi. Ma attento: se il buffer è troppo grande nascondi problemi di backpressure.

Chiudere un channel

Il mittente chiude con close(ch). Il ricevente può iterare con for v := range ch o controllare con la virgola v, ok := <-ch.

func produttore(ch chan<- int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

func main() {
	ch := make(chan int)
	go produttore(ch)
	for v := range ch {
		fmt.Println(v)
	}
}

Regola d’oro: solo il mittente chiude. Mai il ricevente. E non scrivere su un channel chiuso — panic.

Select: il multiplexer dei channel

Serve per attendere più operazioni su channel contemporaneamente. Caso classico: timeout o priorità.

select {
case msg1 := <-ch1:
	fmt.Println("Da ch1:", msg1)
case msg2 := <-ch2:
	fmt.Println("Da ch2:", msg2)
case <-time.After(1 * time.Second):
	fmt.Println("Timeout!")
}

Se più casi sono pronti, Go ne sceglie uno casuale (fairness). Puoi usare default per non bloccarti mai.

Sponsored Protocol

Pattern di concorrenza reali

Worker Pool

Lanci N goroutine che pescano lavoro da un channel. Il main produce lavoro e chiude il channel quando finisce.

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		results <- j * 2 // simula lavoro
	}
}

func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)

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

	// invia 5 job
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)

	// raccogli risultati
	for r := 1; r <= 5; r++ {
		fmt.Println(<-results)
	}
}

Adatto per batch processing, scraping, elaborazione di file.

Fan-In / Fan-Out

  • Fan-Out: un produttore distribuisce lavoro a più goroutine (es. worker pool).
  • Fan-In: più goroutine inviano sullo stesso channel.
func fanIn(ch1, ch2 <-chan string) <-chan string {
	c := make(chan string)
	go func() {
		for {
			select {
			case v := <-ch1:
				c <- v
			case v := <-ch2:
				c <- v
			}
		}
	}()
	return c
}

Perfetto per aggregare risultati da più sorgenti (API, database, file).

Sponsored Protocol

Pipeline con channel

Ogni step è una goroutine che riceve da un channel e produce su un altro. Chaining pulito.

func gen(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out
}

func sq(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			out <- n * n
		}
		close(out)
	}()
	return out
}

func main() {
	c := gen(2, 3, 4)
	out := sq(c)
	for n := range out {
		fmt.Println(n) // 4, 9, 16
	}
}

Gestire cancellazione e timeout con context

Non puoi uccidere una goroutine dall'esterno. Devi cooperare: passare un context.Context e controllare ctx.Done().

func lavoro(ctx context.Context, id int, out chan<- string) {
	for {
		select {
		case <-ctx.Done():
			out <- fmt.Sprintf("worker %d: mi fermo per %v", id, ctx.Err())
			return
		default:
			// lavoro...
		}
	}
}

Usa context.WithTimeout per limiti temporali, context.WithCancel per cancellazione esplicita.

Errori che vediamo in produzione

  • Goroutine leak: goroutine che resta in attesa su un channel mai chiuso. Soluzione: sempre un timeout o ctx.Done().
  • Race condition: accesso a variabili condivise senza mutex. Usa -race nei test.
  • Blocco su channel non bufferizzato: se produttore è unico, ma consumatore non è pronto, deadlock.
  • Copia di WaitGroup: passa sempre puntatore (*sync.WaitGroup), non valore.

In sintesi — cosa fare adesso

  1. Scrivi una goroutine oggi. Prendi un ciclo for loop nel tuo codice e trasformalo in una goroutine con WaitGroup.
  2. Sostituisci le variabili condivise con channel. Se due goroutine si scambiano dati, usa un channel invece di struttura condivisa + mutex.
  3. Aggiungi un timeout a ogni lettura di channel che blocca potenzialmente per sempre. select + time.After.
  4. Testa con go test -race. Il race detector trova accessi non sincronizzati. Fallo diventare parte della tua CI.
  5. Leggi il codice sorgente di net/http. Vedi come Go usa goroutine per ogni connessione. È la migliore didattica.

La concorrenza in Go è un linguaggio dentro il linguaggio. Una volta che padroneggi goroutine e channel, smetti di pensare a thread e lock. Inizi a pensare in pipeline e attori. Ed è lì che il backend scala senza farmi pagare AWS quattro volte tanto.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // WEB AGENCY

Costruiamo la presenza digitale che la tua azienda merita.

Siti web, social, pubblicità online, e-commerce e hosting performante: ingegnerizzati con metodo da ingegneri informatici a Sciacca, per tutta Italia.

> MW_JOURNAL

> READ_ALL()