Canali in Go
I canali (channels) sono il meccanismo principale in Go per la comunicazione tra goroutine. Seguono il principio fondamentale di Go: “Non comunicare condividendo la memoria; condividi la memoria comunicando.”
Creare un Canale
I canali si creano con la funzione built-in make:
ch := make(chan int) // canale di interi (unbuffered)
chStr := make(chan string) // canale di stringhe (unbuffered)
Il tipo chan T indica un canale che trasporta valori di tipo T.
Inviare e Ricevere
L’operatore <- viene usato sia per inviare che per ricevere dati su un canale:
ch <- 42 // invia il valore 42 nel canale
valore := <-ch // riceve un valore dal canale
Ecco un esempio completo:
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Ciao dal canale!" // invia un messaggio
}()
messaggio := <-ch // riceve il messaggio
fmt.Println(messaggio) // stampa "Ciao dal canale!"
}
Canali Unbuffered vs Buffered
Canali Unbuffered
Un canale unbuffered (senza buffer) non ha capacita di memorizzazione. L’invio si blocca finche un’altra goroutine non e pronta a ricevere, e viceversa:
ch := make(chan int) // unbuffered
go func() {
ch <- 42 // si blocca finche qualcuno non riceve
}()
valore := <-ch // si sblocca e riceve 42
I canali unbuffered garantiscono la sincronizzazione tra goroutine.
Canali Buffered
Un canale buffered ha una capacita specificata. L’invio si blocca solo quando il buffer e pieno, e la ricezione si blocca solo quando il buffer e vuoto:
ch := make(chan int, 3) // buffer di capacita 3
ch <- 1 // non si blocca
ch <- 2 // non si blocca
ch <- 3 // non si blocca
// ch <- 4 // si bloccherebbe, il buffer e pieno
fmt.Println(<-ch) // stampa 1
fmt.Println(<-ch) // stampa 2
fmt.Println(<-ch) // stampa 3
Per conoscere la lunghezza e la capacita di un canale si usano len() e cap():
ch := make(chan int, 5)
ch <- 10
ch <- 20
fmt.Println(len(ch)) // 2 (elementi presenti)
fmt.Println(cap(ch)) // 5 (capacita totale)
Direzione dei Canali
Go permette di specificare la direzione di un canale nei parametri delle funzioni, aumentando la sicurezza del codice:
// Canale solo in invio
func produttore(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// Canale solo in ricezione
func consumatore(ch <-chan int) {
for valore := range ch {
fmt.Println("Ricevuto:", valore)
}
}
func main() {
ch := make(chan int)
go produttore(ch) // ch viene convertito a chan<- int
consumatore(ch) // ch viene convertito a <-chan int
}
chan<- T: canale solo in invio (send-only)<-chan T: canale solo in ricezione (receive-only)chan T: canale bidirezionale
Chiudere un Canale
Un canale puo essere chiuso con la funzione close(). Dopo la chiusura, non e piu possibile inviare valori, ma e ancora possibile ricevere i valori rimasti nel buffer:
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
// Possiamo ancora ricevere
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
// Ricevere da un canale chiuso e vuoto restituisce il valore zero
valore, ok := <-ch
fmt.Println(valore, ok) // 0 false
Il secondo valore di ritorno (ok) indica se il canale e ancora aperto e il valore e valido.
Iterare su un Canale con Range
Il costrutto range permette di iterare su un canale fino alla sua chiusura:
package main
import "fmt"
func generaNumeri(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // IMPORTANTE: chiudere il canale per terminare il range
}
func main() {
ch := make(chan int)
go generaNumeri(ch)
for numero := range ch {
fmt.Println(numero)
}
// stampa: 1, 2, 3, 4, 5
}
Attenzione: se non si chiude il canale, il range restera bloccato indefinitamente causando un deadlock.
Scenari di Deadlock
Un deadlock si verifica quando tutte le goroutine sono bloccate in attesa. Ecco gli scenari piu comuni:
Invio senza ricevitore
func main() {
ch := make(chan int)
ch <- 42 // DEADLOCK: nessuna goroutine pronta a ricevere
}
Ricezione senza mittente
func main() {
ch := make(chan int)
valore := <-ch // DEADLOCK: nessuna goroutine invia dati
fmt.Println(valore)
}
Dipendenza circolare
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
val := <-ch1
ch2 <- val
}()
go func() {
val := <-ch2
ch1 <- val
}()
// DEADLOCK: entrambe le goroutine aspettano un valore dall'altra
time.Sleep(time.Second)
}
Pattern Produttore-Consumatore
Un pattern molto comune che sfrutta i canali:
package main
import (
"fmt"
"sync"
)
func produttore(id int, ch chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 3; i++ {
msg := fmt.Sprintf("Produttore %d: messaggio %d", id, i)
ch <- msg
}
}
func main() {
ch := make(chan string, 10)
var wg sync.WaitGroup
// Avvia 3 produttori
for i := 1; i <= 3; i++ {
wg.Add(1)
go produttore(i, ch, &wg)
}
// Chiudi il canale quando tutti i produttori hanno terminato
go func() {
wg.Wait()
close(ch)
}()
// Consuma tutti i messaggi
for msg := range ch {
fmt.Println(msg)
}
}
Conclusione
I canali sono lo strumento fondamentale per la comunicazione tra goroutine in Go. Comprendere la differenza tra canali buffered e unbuffered, la direzione dei canali, e come evitare i deadlock e essenziale per scrivere programmi concorrenti robusti e corretti. Insieme alle goroutine, i canali rappresentano il cuore del modello di concorrenza di Go.