Gestione degli errori in Golang
A differenza dei metodi convenzionali in altri linguaggi di programmazione mainstream come JavaScript (che usa l’istruzione try… catch
) o Python (con il suo blocco try… except
) affrontare gli errori in Go richiede un approccio diverso. Perché? Perché le sue caratteristiche per la gestione degli errori sono spesso mal applicate.
In questo post del blog, daremo un’occhiata alle migliori pratiche che potrebbero essere utilizzate per gestire gli errori in un’applicazione Go. Una comprensione di base di come funziona Go è tutto ciò che è richiesto per digerire questo articolo – se vi sentite bloccati ad un certo punto, va bene prendere un po’ di tempo e ricercare concetti non familiari.
L’identificatore vuoto
L’identificatore vuoto è un segnaposto anonimo. Può essere usato come qualsiasi altro identificatore in una dichiarazione, ma non introduce un legame. L’identificatore vuoto fornisce un modo per ignorare i valori mancanti in un’assegnazione ed evitare errori del compilatore su importazioni e variabili inutilizzate in un programma. La pratica di assegnare gli errori all’identificatore vuoto invece di gestirli correttamente non è sicura in quanto ciò significa che avete deciso di ignorare esplicitamente il valore della funzione definita.
result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}
La ragione per cui probabilmente lo fate è che non vi aspettate un errore dalla funzione (o qualsiasi errore possa verificarsi) ma questo potrebbe creare effetti a cascata nel vostro programma. La cosa migliore da fare è gestire un errore ogni volta che puoi.
Gestire gli errori attraverso valori di ritorno multipli
Un modo per gestire gli errori è sfruttare il fatto che le funzioni in Go supportano valori di ritorno multipli. Così puoi passare una variabile di errore insieme al risultato della funzione che stai definendo:
func iterate(x, y int) (int, error) {}
Nell’esempio di codice sopra, dobbiamo restituire la variabile predefinita error
se pensiamo che ci sia una possibilità che la nostra funzione fallisca. error
è un tipo di interfaccia dichiarato nel pacchetto built-in
di Go e il suo valore zero è nil
.
type error interface { Error() string }
Di solito, restituire un errore significa che c’è un problema e restituire nil
significa che non ci sono stati errori:
result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }
Quindi ogni volta che la funzione iterate
viene chiamata e err
non è uguale a nil
, l’errore restituito dovrebbe essere gestito in modo appropriato – un’opzione potrebbe essere quella di creare un’istanza di un meccanismo di retry o cleanup. L’unico svantaggio nel gestire gli errori in questo modo è che non c’è alcuna applicazione da parte del compilatore di Go, devi decidere come la funzione che hai creato restituisce l’errore. Potete definire una struct di errore e metterla nella posizione dei valori restituiti. Un modo per farlo è usare la struct incorporata errorString
(potete anche trovare questo codice nel codice sorgente di Go):
package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }
Nell’esempio di codice qui sopra, errorString
incorpora un string
che viene restituito dal metodo Error
. Per creare un errore personalizzato, dovrete definire la vostra struct di errore e usare i set di metodi per associare una funzione alla vostra struct:
// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}
L’errore personalizzato appena creato può poi essere ristrutturato per usare la struct incorporata error
:
import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}
Una limitazione della struct incorporata error
è che non ha tracce di stack. Questo rende molto difficile localizzare dove si è verificato un errore. L’errore potrebbe passare attraverso un certo numero di funzioni prima di essere stampato. Per gestire questo, si potrebbe installare il pacchetto pkg/errors
che fornisce primitive di base per la gestione degli errori come la registrazione delle tracce di stack, l’avvolgimento e l’unwrapping degli errori e la formattazione. Per installare questo pacchetto, eseguite questo comando nel vostro terminale:
go get github.com/pkg/errors
Quando avete bisogno di aggiungere tracce dello stack o qualsiasi altra informazione che renda più facile il debug ai vostri errori, usate le funzioni New
o Errorf
per fornire errori che registrano la traccia dello stack. Errorf
implementa l’interfaccia fmt.Formatter
che vi permette di formattare i vostri errori usando le rune del pacchetto fmt
(%s
, %v
, %+v
ecc.):
import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}
Per stampare tracce dello stack invece di un semplice messaggio di errore, dovete usare %+v
invece di %v
nel modello di formato, e le tracce dello stack saranno simili all’esempio di codice qui sotto:
Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471
Defer, panic, and recover
Anche se Go non ha eccezioni, ha un meccanismo simile conosciuto come “Defer, panic, and recover”. L’ideologia di Go è che l’aggiunta di eccezioni come l’istruzione try/catch/finally
in JavaScript porterebbe ad un codice complesso e incoraggerebbe i programmatori ad etichettare troppi errori di base, come non riuscire ad aprire un file, come eccezionali. Non dovreste usare defer/panic/recover
come fareste con throw/catch/finally
; solo in casi di fallimento inaspettato e irrecuperabile.
Defer
è un meccanismo del linguaggio che mette la chiamata di funzione in uno stack. Ogni funzione differita viene eseguita in ordine inverso quando la funzione ospite termina, indipendentemente dal fatto che venga chiamato un panico o meno. Il meccanismo di differimento è molto utile per ripulire le risorse:
package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}
Questo sarebbe compilato come:
If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!
Panic
è una funzione integrata che ferma il normale flusso di esecuzione. Quando chiamate panic
nel vostro codice, significa che avete deciso che il vostro chiamante non può risolvere il problema. Quindi panic
dovrebbe essere usato solo in rari casi in cui non è sicuro per il vostro codice o per chiunque integri il vostro codice continuare in quel punto. Ecco un esempio di codice che mostra come funziona panic
:
package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}
Il campione qui sopra verrebbe compilato come:
If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.
Come mostrato sopra, quando panic
viene usato e non gestito, il flusso di esecuzione si ferma, tutte le funzioni differite vengono eseguite in ordine inverso e vengono stampate le tracce dello stack.
Puoi usare la funzione integrata recover
per gestire panic
e restituire i valori che passano da una chiamata di panico. recover
deve essere sempre chiamato in una funzione defer
altrimenti restituirà nil
:
package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}
Come si può vedere nell’esempio di codice qui sopra, recover
impedisce che l’intero flusso di esecuzione si fermi perché abbiamo gettato in una funzione panic
e il compilatore restituirebbe:
If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.
Per riportare un errore come valore di ritorno, dovete chiamare la funzione recover
nella stessa goroutine in cui viene chiamata la funzione panic
, recuperare una struct di errore dalla funzione recover
e passarla ad una variabile:
package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}
Ogni funzione differita sarà eseguita dopo una chiamata di funzione ma prima di una dichiarazione di ritorno. Quindi, potete impostare una variabile restituita prima che una dichiarazione di ritorno venga eseguita. L’esempio di codice qui sopra compilerebbe come:
If it's more than 100 degrees...Then there's nothing we can doProgram exited.
Error wrapping
Precedentemente l’error wrapping in Go era accessibile solo usando pacchetti come pkg/errors
. Tuttavia, con l’ultima versione di Go, la 1.13, è presente il supporto per l’error wrapping. Secondo le note di rilascio:
Un errore
e
può avvolgere un altro errorew
fornendo un metodoUnwrap
che ritornaw
. Siae
chew
sono disponibili per i programmi, permettendo ae
di fornire un contesto aggiuntivo aw
o di reinterpretarlo, pur permettendo ai programmi di prendere decisioni basate suw
.
Per creare errori avvolti, fmt.Errorf
ora ha un verbo %w
e per ispezionare e scartare gli errori, sono state aggiunte un paio di funzioni al pacchetto error
:
errors.Unwrap
: Questa funzione fondamentalmente ispeziona ed espone gli errori sottostanti in un programma. Restituisce il risultato della chiamata del metodo Unwrap
su Err
. Se il tipo di Err contiene un metodo Unwrap
restituisce un errore. Altrimenti, Unwrap
restituisce nil
.
package errorstype Wrapper interface{ Unwrap() error}
Di seguito un esempio di implementazione del metodo Unwrap
:
func(e*PathError)Unwrap()error{ return e.Err}
errors.Is
: Con questa funzione, potete confrontare un valore di errore con il valore sentinella. Ciò che rende questa funzione diversa dai nostri soliti controlli degli errori è che invece di confrontare il valore sentinella con un errore, lo confronta con ogni errore nella catena degli errori. Implementa anche un metodo Is
su un errore in modo che un errore possa essere postato come sentinella anche se non è un valore sentinella.
func Is(err, target error) bool
Nell’implementazione base di cui sopra, Is
controlla e riporta se err
o uno qualsiasi dei errors
nella sua catena sono uguali a target (valore sentinella).
errors.As
: Questa funzione fornisce un modo per fare un cast su un tipo di errore specifico. Cerca il primo errore nella catena di errori che corrisponde al valore sentinella e, se trovato, imposta il valore sentinella a quel valore di errore e restituisce true
:
package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}
Puoi trovare questo codice nel codice sorgente di Go.
Risultato del compilatore:
Failed at path: non-existingProgram exited.
Un errore corrisponde al valore sentinella se il valore concreto dell’errore è assegnabile al valore indicato dal valore sentinella. As
va in panico se il valore sentinella non è un puntatore non nullo a un tipo che implementa l’errore o a un qualsiasi tipo di interfaccia. As
restituisce false se err
è nil
.
Summario
La comunità Go ha fatto passi da gigante ultimamente con il supporto per vari concetti di programmazione e introducendo modi ancora più concisi e facili per gestire gli errori. Avete qualche idea su come gestire o lavorare con gli errori che possono apparire nel vostro programma Go? Fammi sapere nei commenti qui sotto.
Risorse:
La specifica del linguaggio di programmazione di Go sull’asserzione di tipo
Il discorso di Marcel van Lohuizen a dotGo 2019 – I valori di errore di Go 2 oggi
Note di rilascio di Go 1.13
LogRocket: Visibilità completa nelle tue applicazioni web
LogRocket è una soluzione di monitoraggio delle applicazioni frontend che ti permette di riprodurre i problemi come se fossero accaduti nel tuo browser. Invece di indovinare perché gli errori accadono, o chiedere agli utenti screenshot e log dump, LogRocket ti permette di riprodurre la sessione per capire rapidamente cosa è andato storto. Funziona perfettamente con qualsiasi app, indipendentemente dal framework, e ha plugin per registrare il contesto aggiuntivo da Redux, Vuex, e @ngrx/store.
Oltre a registrare le azioni e lo stato di Redux, LogRocket registra i log della console, gli errori JavaScript, stacktraces, richieste/risposte di rete con intestazioni + corpi, metadati del browser e log personalizzati. Strumenta anche il DOM per registrare l’HTML e il CSS sulla pagina, ricreando video pixel-perfect anche delle più complesse app a pagina singola.
Provalo gratuitamente.