# Concorrenza in Go: Goroutine e Canali - Guida Completa > Padroneggia la concorrenza in Go con goroutine e canali. Pattern avanzati, sincronizzazione, istruzioni select e best practice con esempi di codice dettagliati. - Published: 2026-02-21 - Updated: 2026-04-29 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, channels - Reading time: 14 min --- La concorrenza è uno dei punti di forza maggiori di Go. A differenza di altri linguaggi in cui il multithreading rimane complesso, Go offre un modello elegante basato su goroutine e canali che semplifica notevolmente lo sviluppo di applicazioni concorrenti. > **Filosofia Go** > > "Non comunicare condividendo memoria; condividi memoria comunicando." Questo principio fondamentale guida tutta la progettazione della concorrenza in Go. ## Comprendere le Goroutine Le goroutine sono thread leggeri gestiti dal runtime di Go. Consumano circa 2 KB di stack (rispetto ai diversi MB dei thread del sistema operativo) e permettono di eseguire migliaia di task concorrenti senza sovraccarico. Avviare una goroutine richiede semplicemente di anteporre la parola chiave `go` a una chiamata di funzione. Il runtime si occupa dello scheduling e della distribuzione tra i thread disponibili. ```go // goroutines_basic.go package main import ( "fmt" "time" ) // fetchData simulates a network request func fetchData(id int) { // Simulates network delay time.Sleep(100 * time.Millisecond) fmt.Printf("Data %d fetched\n", id) } func main() { // Sequential execution - 500ms total start := time.Now() for i := 1; i <= 5; i++ { fetchData(i) } fmt.Printf("Sequential: %v\n", time.Since(start)) // Concurrent execution - ~100ms total start = time.Now() for i := 1; i <= 5; i++ { go fetchData(i) // Execute as goroutine } time.Sleep(150 * time.Millisecond) // Wait for completion fmt.Printf("Concurrent: %v\n", time.Since(start)) } ``` L'esecuzione concorrente riduce il tempo totale da 500ms a circa 100ms. Tuttavia, usare `time.Sleep` per sincronizzare le goroutine non è una buona pratica. I canali offrono una soluzione elegante. ## Canali: Comunicazione tra Goroutine Un canale è un condotto tipizzato per inviare e ricevere valori tra goroutine. I canali garantiscono la sincronizzazione: una goroutine che invia attende finché un'altra non riceve, e viceversa. La creazione di un canale utilizza la funzione `make`. L'operatore `<-` invia e riceve dati a seconda della sua posizione rispetto al canale. ```go // channels_basic.go package main import "fmt" // worker performs computation and returns result via channel func worker(id int, jobs <-chan int, results chan<- int) { // Receives jobs until channel closes for job := range jobs { result := job * 2 // Processing results <- result // Send result } } func main() { // Create channels jobs := make(chan int, 10) // Buffered channel results := make(chan int, 10) // Start 3 workers for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // Send 5 jobs for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // Signal end of jobs // Collect results for r := 1; r <= 5; r++ { result := <-results fmt.Printf("Result: %d\n", result) } } ``` I canali direzionali (`<-chan` per ricezione, `chan<-` per invio) rafforzano la sicurezza del codice limitando le operazioni possibili. ## Canali con Buffer vs Senza Buffer La distinzione tra questi tipi di canali influisce direttamente sul comportamento di sincronizzazione tra goroutine. I canali senza buffer bloccano il mittente finché un ricevitore non è pronto. I canali con buffer permettono di inviare fino a N valori senza bloccare, dove N rappresenta la capacità del buffer. ```go // buffered_channels.go package main import "fmt" func main() { // Unbuffered channel - strict synchronization unbuffered := make(chan string) go func() { unbuffered <- "message" // Blocks until received }() msg := <-unbuffered // Unblocks the send fmt.Println(msg) // Buffered channel - capacity of 2 buffered := make(chan int, 2) // These sends don't block buffered <- 1 buffered <- 2 // buffered <- 3 // Would block because buffer is full fmt.Println(<-buffered) // 1 fmt.Println(<-buffered) // 2 // Check capacity fmt.Printf("Length: %d, Capacity: %d\n", len(buffered), cap(buffered)) } ``` I canali con buffer disaccoppiano produttori e consumatori, mentre quelli senza buffer garantiscono una sincronizzazione punto a punto. > **Attenzione ai Deadlock** > > Un deadlock si verifica quando tutte le goroutine sono bloccate in attesa. Il runtime di Go lo rileva e termina il programma con un messaggio di errore esplicito. ## Select: Multiplexing dei Canali L'istruzione `select` attende operazioni simultanee su più canali. Assomiglia a uno switch per le comunicazioni concorrenti. Questa costruzione è essenziale per gestire timeout, cancellazioni e comunicazioni multiple senza bloccarsi indefinitamente su un singolo canale. ```go // select_example.go package main import ( "fmt" "time" ) // fetchAPI simulates an API call with variable delay func fetchAPI(name string, delay time.Duration, ch chan<- string) { time.Sleep(delay) ch <- fmt.Sprintf("%s: data received", name) } func main() { api1 := make(chan string) api2 := make(chan string) // Launch two API calls in parallel go fetchAPI("API-1", 100*time.Millisecond, api1) go fetchAPI("API-2", 200*time.Millisecond, api2) // Global timeout of 150ms timeout := time.After(150 * time.Millisecond) // Collect results with timeout for i := 0; i < 2; i++ { select { case result := <-api1: fmt.Println(result) case result := <-api2: fmt.Println(result) case <-timeout: fmt.Println("Timeout - operation cancelled") return } } } ``` Il `select` sceglie il primo canale disponibile. Se più canali sono pronti, la scelta è pseudo-casuale per evitare lo starvation. ## Pattern Worker Pool Il pattern worker pool distribuisce le task tra più worker, limitando la concorrenza e ottimizzando l'uso delle risorse. Questo pattern è indispensabile per elaborare grandi quantità di dati. L'implementazione si basa su un canale di task condiviso tra i worker e un canale di risultati per la raccolta. ```go // worker_pool.go package main import ( "fmt" "sync" "time" ) // Task represents a unit of work type Task struct { ID int Data string } // Result contains the processing result type Result struct { TaskID int Output string } // worker processes received tasks func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) { defer wg.Done() for task := range tasks { // Simulate processing time.Sleep(50 * time.Millisecond) results <- Result{ TaskID: task.ID, Output: fmt.Sprintf("Worker %d processed: %s", id, task.Data), } } } func main() { const numWorkers = 3 const numTasks = 10 tasks := make(chan Task, numTasks) results := make(chan Result, numTasks) var wg sync.WaitGroup // Start workers for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, tasks, results, &wg) } // Send tasks for i := 1; i <= numTasks; i++ { tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)} } close(tasks) // Close results channel after workers finish go func() { wg.Wait() close(results) }() // Collect results for result := range results { fmt.Printf("Task %d: %s\n", result.TaskID, result.Output) } } ``` Il `sync.WaitGroup` coordina l'attesa di tutti i worker prima di chiudere il canale dei risultati. ## Pattern Fan-Out/Fan-In Questo pattern distribuisce il lavoro tra più goroutine (fan-out) e poi aggrega i risultati (fan-in). Massimizza il parallelismo semplificando la raccolta dei risultati. ```go // fan_out_fan_in.go package main import ( "fmt" "sync" ) // generate produces numbers on a channel func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square computes the square of received numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // merge combines multiple channels into one (fan-in) func merge(channels ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup // Output function for each channel output := func(c <-chan int) { defer wg.Done() for n := range c { out <- n } } // Launch a goroutine per channel wg.Add(len(channels)) for _, c := range channels { go output(c) } // Close after all goroutines finish go func() { wg.Wait() close(out) }() return out } func main() { // Generate data numbers := generate(1, 2, 3, 4, 5, 6, 7, 8) // Fan-out: distribute to 3 workers sq1 := square(numbers) sq2 := square(numbers) sq3 := square(numbers) // Fan-in: aggregate results for result := range merge(sq1, sq2, sq3) { fmt.Println(result) } } ``` Questo pattern eccelle per operazioni CPU-bound distribuibili e pipeline di elaborazione dati. ## Context per Cancellazione e Deadline Il package `context` standardizza la gestione di cancellazioni, deadline e valori tra goroutine. Qualsiasi goroutine di lunga durata dovrebbe accettare un context come primo parametro. ```go // context_example.go package main import ( "context" "fmt" "time" ) // fetchWithContext simulates a cancellable request func fetchWithContext(ctx context.Context, url string) (string, error) { // Simulates a long operation select { case <-time.After(2 * time.Second): return fmt.Sprintf("Data from %s", url), nil case <-ctx.Done(): return "", ctx.Err() // context.Canceled or context.DeadlineExceeded } } func main() { // Context with 500ms timeout ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // Release resources result, err := fetchWithContext(ctx, "https://api.example.com") if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Println(result) // Context with manual cancellation ctx2, cancel2 := context.WithCancel(context.Background()) go func() { time.Sleep(100 * time.Millisecond) cancel2() // Explicit cancellation }() result, err = fetchWithContext(ctx2, "https://api2.example.com") if err != nil { fmt.Printf("Request cancelled: %v\n", err) } } ``` > **Buona Pratica** > > Chiamare sempre `defer cancel()` immediatamente dopo aver creato un context per evitare leak di risorse. ## Sincronizzazione con sync.Mutex Sebbene i canali siano preferibili per la comunicazione, il package `sync` rimane necessario per proteggere l'accesso concorrente a strutture dati condivise. ```go // mutex_example.go package main import ( "fmt" "sync" ) // SafeCounter is a thread-safe counter type SafeCounter struct { mu sync.Mutex value map[string]int } // Increment increments the value for a given key func (c *SafeCounter) Increment(key string) { c.mu.Lock() // Exclusive lock defer c.mu.Unlock() // Guaranteed unlock c.value[key]++ } // Value returns the current value func (c *SafeCounter) Value(key string) int { c.mu.Lock() defer c.mu.Unlock() return c.value[key] } func main() { counter := SafeCounter{value: make(map[string]int)} var wg sync.WaitGroup // 1000 concurrent increments for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment("visits") }() } wg.Wait() fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000 } ``` Il `sync.RWMutex` ottimizza le letture concorrenti con `RLock()`/`RUnlock()` per operazioni di sola lettura. ## Errori Comuni e Soluzioni La concorrenza in Go presenta insidie classiche. Ecco gli errori più frequenti e come evitarli. ```go // common_mistakes.go package main import ( "fmt" "sync" ) func main() { // ERROR: Loop variable capture // All goroutines would print the same value for i := 0; i < 3; i++ { go func() { fmt.Println(i) // Capture by reference - BUG }() } // SOLUTION: Pass value as parameter var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Println(n) // Local copy - CORRECT }(i) } wg.Wait() // ERROR: Send on nil channel var ch chan int // ch <- 1 // Blocks forever // SOLUTION: Always initialize with make ch = make(chan int, 1) ch <- 1 fmt.Println(<-ch) // ERROR: Send on closed channel done := make(chan bool) close(done) // done <- true // Panic! // SOLUTION: Check before send or use sync.Once select { case done <- true: fmt.Println("Sent") default: fmt.Println("Channel closed or full") } } ``` Il rilevamento di race condition usa il flag `-race` durante la compilazione o i test: `go test -race ./...`. ## Conclusione Padroneggiare la concorrenza in Go si basa su alcuni concetti chiave che, una volta ben compresi, permettono di costruire applicazioni ad alte prestazioni. **Punti chiave:** ✅ Le goroutine sono leggere ed economiche - crearne migliaia rimane accettabile ✅ I canali sincronizzano e trasferiscono dati tra goroutine ✅ L'istruzione `select` gestisce comunicazioni multiple e timeout ✅ Il pattern worker pool limita la concorrenza e ottimizza le risorse ✅ Il package `context` standardizza cancellazione e deadline ✅ I mutex proteggono i dati condivisi quando i canali non bastano ✅ Il flag `-race` rileva le race condition durante i test La filosofia "Condividere memoria comunicando" guida verso design più sicuri e manutenibili rispetto al multithreading tradizionale con lock. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/go/go-concurrency-goroutines-channels