Panic e Recover in Go
In Go, panic e recover sono meccanismi per gestire situazioni eccezionali e irrecuperabili. A differenza delle eccezioni in altri linguaggi, in Go si preferisce la gestione esplicita degli errori; panic e recover sono riservati a casi veramente critici.
Cos’e il Panic
Un panic interrompe immediatamente l’esecuzione normale della funzione corrente. Tutte le funzioni defer vengono eseguite, poi il programma risale lo stack delle chiamate ripetendo il processo fino a terminare con un messaggio di errore:
package main
import "fmt"
func main() {
fmt.Println("Inizio")
panic("qualcosa e andato molto male!")
fmt.Println("Questa riga non viene mai eseguita")
}
Output:
Inizio
panic: qualcosa e andato molto male!
goroutine 1 [running]:
main.main()
/main.go:6 +0x...
exit status 2
Quando si Verifica un Panic
Go genera automaticamente un panic in diverse situazioni:
// Accesso fuori dai limiti di uno slice
s := []int{1, 2, 3}
fmt.Println(s[10]) // panic: runtime error: index out of range
// Dereferenziazione di un puntatore nil
var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address
// Invio su un canale chiuso
ch := make(chan int)
close(ch)
ch <- 42 // panic: send on closed channel
// Asserzione di tipo errata
var i interface{} = "ciao"
n := i.(int) // panic: interface conversion: string, not int
Usare Recover con Defer
recover e una funzione built-in che permette di “catturare” un panic e riprendere il controllo. Funziona solo all’interno di una funzione defer:
package main
import "fmt"
func funzionePericolosa() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recuperato dal panic:", r)
}
}()
fmt.Println("Prima del panic")
panic("errore critico!")
fmt.Println("Dopo il panic") // non viene mai eseguito
}
func main() {
funzionePericolosa()
fmt.Println("Il programma continua normalmente")
}
Output:
Prima del panic
Recuperato dal panic: errore critico!
Il programma continua normalmente
Recover Restituisce un Valore
recover() restituisce il valore passato a panic(). Se non c’e stato un panic, restituisce nil:
defer func() {
r := recover()
if r == nil {
fmt.Println("Nessun panic")
return
}
// Il valore puo essere di qualsiasi tipo
switch v := r.(type) {
case string:
fmt.Println("Panic con messaggio:", v)
case error:
fmt.Println("Panic con errore:", v)
default:
fmt.Println("Panic con valore sconosciuto:", v)
}
}()
Quando Usare Panic vs Error
Usa error (la norma in Go)
- Per condizioni di errore prevedibili e gestibili
- Per errori legati all’input dell’utente
- Per errori di rete, file non trovati, ecc.
- Per qualsiasi errore che il chiamante puo voler gestire
func dividi(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisione per zero")
}
return a / b, nil
}
Usa panic (eccezione, non la norma)
- Per errori di programmazione (bug nel codice)
- Per condizioni che non dovrebbero mai verificarsi
- Per l’inizializzazione di un programma che non puo procedere
- Per violazioni di invarianti interne
func deveEsserePositivo(n int) {
if n <= 0 {
panic(fmt.Sprintf("valore deve essere positivo, ricevuto: %d", n))
}
}
Pattern nel Mondo Reale
Proteggere un Server HTTP
Un pattern molto comune e proteggere i gestori HTTP da panic imprevisti:
package main
import (
"fmt"
"log"
"net/http"
)
func middlewareRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Panic nel gestore: %v", err)
http.Error(w, "Errore interno del server", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func gestorePericoloso(w http.ResponseWriter, r *http.Request) {
panic("qualcosa e andato storto!")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", gestorePericoloso)
server := middlewareRecovery(mux)
fmt.Println("Server in ascolto su :8080")
log.Fatal(http.ListenAndServe(":8080", server))
}
Convertire Panic in Errore
Un pattern utile per librerie che vogliono trasformare un panic interno in un errore restituito:
func operazioneSicura() (err error) {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
err = fmt.Errorf("operazione fallita: %w", v)
default:
err = fmt.Errorf("operazione fallita: %v", v)
}
}
}()
// codice che potrebbe generare un panic
eseguiOperazioneRischiosa()
return nil
}
Init con Must
Un pattern comune per l’inizializzazione e il prefisso Must, che genera un panic se l’inizializzazione fallisce:
func MustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(fmt.Sprintf("pattern non valido: %s: %v", pattern, err))
}
return re
}
// Usato a livello di pacchetto
var emailRegex = MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
Questo pattern e usato nella libreria standard di Go, ad esempio in regexp.MustCompile e template.Must.
Conclusione
Panic e recover sono strumenti potenti ma da usare con parsimonia in Go. La filosofia del linguaggio privilegia la gestione esplicita degli errori tramite valori di ritorno. Usa panic solo per errori di programmazione e condizioni veramente irrecuperabili, e recover per proteggere i confini critici del programma come i server HTTP. Seguire questo principio porta a codice Go piu robusto e prevedibile.