Menu

Channel in Golang: bufferizzati, non bufferizzati, close e range

Come i channel di Go passano valori tra goroutine: channel non bufferizzati e bufferizzati, chiusura e range, tipi direzionali, l'errore di deadlock e una pipeline costruita con i channel.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Inviare e ricevere

Un channel è un condotto tipizzato tra goroutine. ch <- v invia, <-ch riceve. Se ne crea uno con make:

Il valore zero di un tipo channel è nil, quindi var ch chan string senza make ti dà un channel che si blocca per sempre. Crea sempre i channel con make.

I channel non bufferizzati sincronizzano

make(chan T) crea un channel non bufferizzato. Un invio si blocca finché un ricevente non prende il valore, e una ricezione si blocca finché un mittente non ne fornisce uno. Le due goroutine si incontrano in quel punto, il che rende un channel non bufferizzato uno strumento di sincronizzazione tanto quanto un condotto di dati. Quando <-done ritorna qui sotto, sai che il worker ha finito tutto ciò che precede il suo invio:

Qui leggere result in main è sicuro anche senza mutex. Il modello di memoria di Go garantisce che tutto ciò che il worker ha fatto prima dell'invio sia visibile a main dopo la ricezione corrispondente. chan struct{} è il tipo idiomatico per un semplice segnale, perché struct{} non occupa memoria.

Channel bufferizzati

make(chan T, n) dà al channel spazio per n valori. Gli invii riescono senza un ricevente finché il buffer non è pieno; le ricezioni riescono finché non è vuoto. I valori escono nell'ordine in cui sono entrati.

Un buffer disaccoppia mittente e ricevente, così brevi raffiche non bloccano il mittente. Non risolve il caso di un produttore sempre più veloce del suo consumatore; rimanda soltanto il momento in cui il mittente si blocca. Scegli la dimensione del buffer per un motivo preciso (il numero di mittenti, una dimensione di batch nota), non per far sparire un deadlock.

len(ch) è un'istantanea. Quando agisci in base al suo valore, un'altra goroutine potrebbe averlo già cambiato, quindi non usarlo per decidere se un invio si bloccherà. Per questo usa select con un caso default.

Close e range

close(ch) dice ai riceventi che non verranno inviati altri valori. Dopo una chiusura:

  • i valori già nel buffer vengono comunque consegnati,
  • poi ogni ricezione restituisce subito il valore zero,
  • v, ok := <-ch riporta ok == false,
  • for v := range ch termina.

La prima ricezione dopo close riceve ancora "last" con ok == true. La seconda riceve il valore zero "" e false.

Regole che provocano un panic:

  • inviare su un channel chiuso provoca il panic send on closed channel,
  • chiudere un channel già chiuso provoca un panic,
  • chiudere un channel nil provoca un panic.

Quindi chiude solo il lato che invia, e una volta sola. Con più mittenti, nessun mittente sa da solo quando gli altri hanno finito; fai attendere tutti i mittenti a una goroutine separata (con un sync.WaitGroup) e chiudi il channel dopo che Wait è ritornato. La chiusura serve solo quando un ricevente aspetta la fine. Un channel non chiuso che nessuno referenzia viene raccolto dal garbage collector come qualsiasi altro valore.

Tipi direzionali

Una funzione può dichiarare che su un channel si limita a inviare o a ricevere. Il compilatore rifiuta allora l'altra operazione.

TipoSignificatoConsentito
chan Tbidirezionaleinvio, ricezione, chiusura
chan<- Tsolo invioinvio, chiusura
<-chan Tsolo ricezionericezione

Un chan T si converte implicitamente in uno dei due tipi ristretti quando lo passi a una funzione. Per questo produce qui sopra può restituire un <-chan int: chi lo chiama può fare il range su di esso ma non può inviarci valori né chiuderlo. Usa i tipi direzionali su ogni parametro di funzione dove si applicano. Documentano chi possiede il channel e trasformano un uso scorretto in un errore di compilazione.

Deadlock

Se tutte le goroutine sono bloccate e niente può risvegliarne nessuna, il runtime interrompe il programma:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:7 +0x38
exit status 2

Il dump delle goroutine ti dice quale operazione è bloccata (qui chan send, oppure chan receive, sync.WaitGroup.Wait, select). Le cause tipiche:

  • un invio su un channel non bufferizzato senza nessun ricevente in esecuzione,
  • range su un channel che non viene mai chiuso,
  • un contatore di WaitGroup che non arriva mai a zero,
  • due goroutine che si aspettano a vicenda.

Il runtime rileva solo il caso in cui tutte le goroutine sono bloccate. In un server con altre goroutine attive (un listener HTTP, un ticker), lo stesso bug non fa crashare nulla; la goroutine bloccata rimane semplicemente appesa, sprecando risorse.

Channel nil

Invii e ricezioni su un channel nil si bloccano per sempre. Sembra inutile, ma è un trucco classico dentro select: impostare una variabile channel a nil disattiva il suo caso. Questo codice unisce due channel e smette di ascoltare ciascuno appena viene chiuso:

Senza le assegnazioni a nil, un channel chiuso è sempre pronto e il ciclo girerebbe a vuoto sui valori zero.

Una pipeline

I channel si compongono in pipeline: ogni fase è una goroutine che riceve da un channel e invia al successivo, e chiude il proprio output quando il suo input è terminato.

Ogni fase gira in modo concorrente e l'ordine dell'output è deterministico (1, 16, 81), perché ogni fase è una singola goroutine che preserva l'ordine. Le chiusure si propagano a cascata: generate chiude, il che termina il range di square, che chiude il proprio output, e così via fino a main.

Il punto debole di questa pipeline: se main smettesse di leggere in anticipo, le fasi resterebbero bloccate per sempre sui loro invii. Le pipeline reali ricevono un context.Context o un channel done e fanno select su di esso accanto a ogni invio.

Channel o mutex

I channel servono a passare la proprietà dei dati e a segnalare eventi. Un sync.Mutex è più semplice per proteggere uno stato condiviso che molte goroutine leggono e aggiornano sul posto, come una cache o un contatore. Una struct con un mutex al suo interno è spesso più chiara di una goroutine che possiede lo stato e serve richieste tramite channel. Usa la soluzione che rende il codice più corto e la proprietà evidente.

Riferimento rapido

Operazionechannel nilchannel apertochannel chiuso
ch <- vsi blocca per sempresi blocca finché il valore non viene ricevuto o il buffer ha spaziopanic
<-chsi blocca per sempresi blocca finché non c'è un valore disponibilevalori nel buffer, poi valore zero
v, ok := <-chsi blocca per sempreok è trueok è false una volta svuotato
close(ch)panicchiudepanic
len(ch), cap(ch)0, 0valori nel buffer, dimensione del buffervalori rimasti, dimensione del buffer

Domande frequenti

Qual è la differenza tra un channel bufferizzato e uno non bufferizzato in Go?

Un channel non bufferizzato (make(chan int)) non ha spazio di memoria: un invio si blocca finché un'altra goroutine non riceve, quindi ogni invio è anche un passaggio di mano e un punto di sincronizzazione. Un channel bufferizzato (make(chan int, 3)) contiene fino a 3 valori; gli invii si bloccano solo quando il buffer è pieno e le ricezioni solo quando è vuoto.

Cosa succede quando leggi da un channel chiuso in Go?

Le ricezioni su un channel chiuso non si bloccano mai. Prima svuotano i valori rimasti nel buffer, poi restituiscono per sempre il valore zero del tipo dell'elemento. Usa v, ok := <-ch per distinguere i due casi: ok vale false quando il channel è chiuso e vuoto. Un ciclo for v := range ch si ferma a quel punto.

Chi deve chiudere un channel in Go?

Il mittente, e solo quando i riceventi devono sapere che non arriveranno altri valori (per esempio per terminare un ciclo range). Inviare su un channel chiuso provoca un panic, e lo stesso vale per chiudere due volte un channel, quindi un ricevente che chiude il channel entra in corsa con i mittenti. Non serve chiudere un channel per liberarlo; il garbage collector recupera comunque i channel non più raggiungibili.

Cosa significa "fatal error: all goroutines are asleep"?

Ogni goroutine del programma è bloccata su un'operazione di channel o su un lock che niente potrà mai completare, quindi il runtime ferma il programma. La causa più comune è inviare su un channel non bufferizzato in main senza un'altra goroutine che riceve, oppure fare il range su un channel che non viene mai chiuso.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA