00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.