Com'è fatto un panic
Un panic ferma la funzione corrente, esegue le sue chiamate in defer, poi fa lo stesso in chi l'ha chiamata, e così via risalendo lo stack. Se arriva in cima alla goroutine, il programma crasha.
Questo programma termina con stato 2. L'output è:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
La chiamata in defer è stata eseguita prima del report del crash. Il trace indica la goroutine, la funzione e la riga, che di solito basta per trovare il bug.
I panic di runtime più comuni
| Messaggio | Causa |
|---|---|
index out of range [5] with length 3 | indice di slice, array o stringa oltre la fine |
slice bounds out of range [:7] with capacity 5 | slicing oltre la capacità |
invalid memory address or nil pointer dereference | leggere un campo o chiamare tramite un puntatore nil |
assignment to entry in nil map | scrivere in una map mai creata |
interface conversion: interface {} is int, not string | type assertion a un solo valore verso il tipo sbagliato |
integer divide by zero | divisione intera o modulo per 0 (i float danno invece +Inf o NaN) |
close of closed channel, send on closed channel | uso scorretto di un channel |
all goroutines are asleep - deadlock! | tutte le goroutine bloccate (un errore fatale, non un panic) |
Ognuno di questi è un bug nel programma, non una condizione da gestire. La soluzione è un controllo dei limiti, un controllo su nil, un make o una assertion comma-ok, non un recover.
Recuperare
recover() ferma un panic. Funziona solo quando viene chiamato direttamente dentro una funzione in defer, perché le funzioni in defer sono l'unico codice che gira mentre un panic risale lo stack.
Output:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Cosa è successo nella seconda chiamata:
a / bè andato in panic.- La closure in defer è stata eseguita, e
recover()ha restituito il valore del panic (unruntime.Error). - La risalita si è fermata.
safeDivideè tornata normalmente amain, con il risultato con nomeerrimpostato dalla closure.
È il risultato con nome che permette alla funzione in defer di restituire un errore. Senza, la funzione restituisce i suoi zero value. La pagina su defer spiega come le closure in defer modificano i risultati.
recover() restituisce nil quando non c'è nessun panic, quindi il controllo if r != nil rende innocua la funzione in defer nel percorso normale. Chiamato fuori da una funzione in defer, o in una funzione chiamata dalla funzione in defer, recover restituisce nil e non fa niente.
panic con un tuo valore
panic accetta qualsiasi valore. Un errore o una stringa sono i casi tipici.
Recupera ciò che ti aspetti e rilancia il panic per tutto il resto. Inghiottire ogni panic nasconde bug reali.
Da Go 1.21, panic(nil) viene trasformato in un *runtime.PanicNilError, quindi un recover() che restituisce nil ora significa in modo affidabile "nessun panic".
Panic nelle goroutine
recover cattura solo i panic della propria goroutine. Un panic in qualsiasi goroutine senza recover uccide l'intero processo, compresi main e ogni altra goroutine.
Le due righe dei worker possono comparire in qualsiasi ordine; main finished arriva sempre per ultima. Un defer recover() in main non avrebbe salvato il programma dal secondo worker. Per questo i server HTTP fanno recover per ogni richiesta: net/http recupera i panic nella goroutine di ogni handler, li registra nel log e chiude quella connessione, così una richiesta sbagliata non butta giù il server.
Alcuni fallimenti sono errori fatali, non panic, e non si possono recuperare in nessun modo: concurrent map writes, la memoria esaurita e il messaggio all goroutines are asleep del rilevatore di deadlock.
Quando andare in panic è la scelta giusta
La regola generale di Go: restituisci errori per tutto ciò che può andare storto a runtime, e vai in panic solo per gli errori di chi programma. In concreto, il panic è appropriato quando:
- Un invariante è rotto. Uno
switchsul tuo enum raggiunge un caso che non può succedere. Continuare corromperebbe i dati. - Un helper
Mustriceve un input costante sbagliato.regexp.MustCompile,template.Musteuuid.MustParseavvolgono una funzione che restituisce un errore e vanno in panic in caso di fallimento. Usali per valori noti in fase di compilazione, tipicamente variabili a livello di package, dove un fallimento significa che il codice sorgente è sbagliato:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- L'avvio non può proseguire. Manca una configurazione obbligatoria in
main. Anche qui, stampare l'errore e chiamareos.Exit(1)è spesso più pulito di uno stack trace.
Il panic è lo strumento sbagliato per:
- Fallimenti previsti: input non valido dell'utente, un file mancante, un timeout. Restituisci un
error; vedi gestione degli errori. - Controllo del flusso: usare panic e recover come eccezioni attraverso un grande albero di chiamate rende il codice difficile da seguire. La libreria standard lo fa internamente in un paio di punti (l'encoder di
encoding/json), recuperando sempre prima di restituire, così nessun panic esce dal package. - API di libreria: una libreria che va in panic con un input sbagliato costringe ogni chiamante ad aggiungere dei recover. Restituisci un errore.
Errori comuni
- Chiamare recover fuori da una funzione in defer. Restituisce
nil. - Fare recover in
mainper il panic di una goroutine. Ogni goroutine ha bisogno del suo. - Inghiottire tutti i panic. Registra nel log con lo stack (
debug.Stack()daruntime/debug) e rilancia il panic per ciò che non ti aspettavi. - Usare
recoverper gestire scritture in map nil o indici fuori range. Correggi invece il bug.
Domande frequenti
Cos'è un panic in Go?
Un fallimento a runtime che ferma il flusso normale della goroutine corrente. Go esegue le chiamate in defer di ogni funzione sullo stack, dalla più interna verso l'esterno, e se niente lo recupera, il programma stampa il valore del panic e uno stack trace e termina con stato 2. I panic nascono da bug (indice fuori range, dereferenziazione di un puntatore nil, scrittura in una map nil) o da una chiamata esplicita panic(v).
Come si recupera da un panic in Go?
Chiama recover() dentro una funzione in defer: defer func() { if r := recover(); r != nil { ... } }(). Restituisce il valore passato a panic e ferma la risalita, così la funzione che l'ha messo in defer ritorna normalmente a chi l'ha chiamata. Chiamato in qualsiasi altro punto, recover restituisce nil e non fa niente.
Posso recuperare un panic da un'altra goroutine?
No. recover ferma solo un panic nella goroutine in cui viene eseguito. Un panic in una goroutine che hai avviato, senza recover dentro quella goroutine, fa crashare l'intero programma. Ogni goroutine che potrebbe andare in panic ha bisogno del suo recover in defer.
Quando dovrei usare panic invece di restituire un errore?
Per i bug e gli stati impossibili, non per i fallimenti previsti. Input sbagliati, file mancanti ed errori di rete sono errori. Il panic è ragionevole quando un invariante è rotto, quando a un helper Must viene passata una costante che dovrebbe essere sempre valida (regexp.MustCompile), o quando il programma non può proprio partire. Le librerie non dovrebbero lasciar uscire panic dalla loro API pubblica.