Ο χειρισμός σφαλμάτων δεν γίνεται μέσω εξαιρέσεων στη Go.
Αντ' αυτού, τα σφάλματα είναι κανονικές τιμές τύπων που υλοποιούν την ενσωματωμένη διεπαφή error.
Η διεπαφή error είναι πολύ λιτή.
Περιέχει μόνο μία μέθοδο, τηv Error(), που επιστρέφει το μήνυμα σφάλματος ως συμβολοσειρά.
type error interface {
Error() string
}
Κάθε φορά που ορίζεις μια συνάρτηση στην οποία μπορεί να προκύψει ένα σφάλμα κατά την εκτέλεση που πρέπει να φτάσει σε αυτόν που κάλεσε τη συνάρτηση, πρέπει να συμπεριλάβεις το error ως έναν από τους τύπους επιστροφής.
Αν η συνάρτηση έχει πολλαπλές τιμές επιστροφής, κατά σύμβαση το error είναι πάντα το τελευταίο.
func DoSomething() (int, error) {
// ...
}
Δεν χρειάζεται να υλοποιείς πάντα μόνος σου τη διεπαφή error.
Για να δημιουργήσεις ένα απλό σφάλμα, μπορείς να χρησιμοποιήσεις τη συνάρτηση errors.New(), που είναι μέρος του πακέτου errors της τυπικής βιβλιοθήκης.
Το μόνο που χρειάζεται να περάσεις είναι το μήνυμα σφάλματος ως συμβολοσειρά, και το errors.New() θα αναλάβει να δημιουργήσει μια τιμή που περιέχει το μήνυμά σου και υλοποιεί τη διεπαφή error.
Αν η συνάρτηση επιστρέφει σφάλμα, καλή πρακτική είναι να επιστρέφεις τη μηδενική τιμή για όλες τις άλλες παραμέτρους επιστροφής:
func DoSomething() (SomeStruct, int, error) {
// ...
return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Δεν πρέπει να υποθέτεις ότι όλες οι συναρτήσεις επιστρέφουν μηδενικές τιμές για τις άλλες τιμές επιστροφής όταν υπάρχει σφάλμα. Ως βέλτιστη πρακτική, να υποθέτεις ότι δεν είναι ασφαλές να χρησιμοποιήσεις καμία από τις άλλες τιμές επιστροφής αν επιστραφεί σφάλμα.
Για παράδειγμα, φαντάσου μια συνάρτηση που προσπαθούσε να διαβάσει από ένα αρχείο και επιστρέφει ένα μισογεμάτο slice από byte και ένα σφάλμα. Αν επαναχρησιμοποιήσεις αυτό το slice από byte που επιστράφηκε στον κώδικά σου, υποθέτοντας ότι είναι πάντα κενό επειδή υπήρξε σφάλμα, μπορεί να πέσεις πάνω σε ύπουλα bugs.
Οι μόνες εξαιρέσεις είναι περιπτώσεις όπου η τεκμηρίωση της συνάρτησης δηλώνει ξεκάθαρα ότι οι άλλες τιμές επιστροφής έχουν νόημα σε περίπτωση σφάλματος.
Δες την [τεκμηρίωση για τη Write σε έναν buffer][buffer-write] για ένα παράδειγμα.
Φρόντισε το μήνυμα σφάλματος που δίνεις να είναι όσο πιο συγκεκριμένο γίνεται, καθώς τα σφάλματα δεν περιλαμβάνουν κανένα ίχνος στοίβας εξ ορισμού. Κατά σύμβαση, το μήνυμα σφάλματος πρέπει να ξεκινά με πεζό γράμμα και να μην τελειώνει με τελεία.
Αν θέλεις να χρησιμοποιήσεις ένα τέτοιο σφάλμα σε πολλά σημεία (ή θέλεις να κάνεις το σφάλμα διαθέσιμο στον καταναλωτή του πακέτου σου), θα πρέπει να δηλώσεις μια μεταβλητή για το σφάλμα αντί να χρησιμοποιείς το errors.New επί τόπου.
Κατά σύμβαση, το όνομα της μεταβλητής πρέπει να ξεκινά με Err ή err (ανάλογα με το αν είναι εξαγόμενη ή όχι).
Αυτές οι μεταβλητές σφάλματος συχνά ονομάζονται σφάλματα-φρουροί.
import "errors"
var ErrNotFound = errors.New("resource was not found")
func DoSomething() error {
// ...
return ErrNotFound
}
Επίστρεψε nil για το σφάλμα για να δηλώσεις ότι δεν υπήρξαν σφάλματα κατά την εκτέλεση της συνάρτησης:
func Foo() (int, error) {
return 10, nil
}
Αν χρειάζεται να κατασκευάσεις ένα πιο σύνθετο μήνυμα σφάλματος από κάποιες μεταβλητές, μπορείς να αξιοποιήσεις τη μορφοποίηση συμβολοσειρών.
Η Go έχει μια ειδική συνάρτηση, τηv fmt.Errorf, γι' αυτόν τον σκοπό.
Κατασκευάζει τη συμβολοσειρά του μηνύματος σφάλματος και επιστρέφει ένα σφάλμα.
input := 123
action := "UPDATE"
err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"
Αν καλέσεις μια συνάρτηση που επιστρέφει σφάλμα, συνηθίζεται να αποθηκεύεις την τιμή του σφάλματος σε μια μεταβλητή που ονομάζεται err.
Πριν χρησιμοποιήσεις το πραγματικό αποτέλεσμα της συνάρτησης, πρέπει να ελέγξεις ότι δεν υπήρξε σφάλμα.
Μπορούμε να χρησιμοποιήσουμε τα == και != για να συγκρίνουμε το σφάλμα με το nil και ξέρουμε ότι υπήρξε σφάλμα όταν το err δεν είναι nil.
Για να αποφύγεις την εμφώλευση της "καλής διαδρομής" του κώδικά σου, οι περιπτώσεις σφάλματος θα πρέπει να αντιμετωπίζονται πρώτες, συνήθως με μια πρόωρη επιστροφή. Δες αυτό το διάσημο άρθρο του Mat Ryer για περισσότερες λεπτομέρειες.
// Do this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
return err
}
// do something with file
}
// Not this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
// handle err
} else {
// do something with file
}
}
// Also not this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err == nil {
// do something with file
}
// handle err
}
Τις περισσότερες φορές, το σφάλμα θα επιστραφεί προς τα πάνω στη στοίβα των συναρτήσεων, όπως φαίνεται στο πρώτο παράδειγμα παραπάνω. Ένας άλλος τρόπος να χειριστείς το σφάλμα θα ήταν να το καταγράψεις και να συνεχίσεις με κάποια άλλη ενέργεια. Καλή πρακτική είναι είτε να επιστρέφεις είτε να καταγράφεις το σφάλμα, ποτέ και τα δύο.
Επειδή οι περισσότερες συναρτήσεις στη Go περιλαμβάνουν ένα σφάλμα ως μία από τις τιμές επιστροφής, θα δεις και θα χρησιμοποιήσεις το μοτίβο if err != nil παντού στον κώδικα Go.
Αν θέλεις το σφάλμα σου να περιλαμβάνει περισσότερες πληροφορίες από τη συμβολοσειρά του μηνύματος σφάλματος, μπορείς να δημιουργήσεις έναν προσαρμοσμένο τύπο σφάλματος.
Όπως αναφέρθηκε προηγουμένως, ό,τι υλοποιεί τη διεπαφή error (δηλαδή έχει μια μέθοδο Error() string) μπορεί να λειτουργήσει ως σφάλμα στη Go.
Συνήθως, χρησιμοποιείται ένα struct για τη δημιουργία ενός προσαρμοσμένου τύπου σφάλματος.
Κατά σύμβαση, τα ονόματα των προσαρμοσμένων τύπων σφάλματος πρέπει να τελειώνουν με Error.
Επίσης, είναι καλύτερο να ορίσεις τη μέθοδο Error() string με δέκτη δείκτη· δες αυτό το σχόλιο στο Stackoverflow για να καταλάβεις το σκεπτικό.
Σημείωσε ότι αυτό σημαίνει πως πρέπει να επιστρέψεις έναν δείκτη στο προσαρμοσμένο σφάλμα σου, αλλιώς δεν θα μετράει ως error, επειδή η τιμή χωρίς δείκτη δεν παρέχει τη μέθοδο Error() string.
type MyCustomError struct {
message string
details string
}
func (e *MyCustomError) Error() string {
return fmt.Sprintf("%s, details: %s", e.message, e.details)
}
func someFunction() error {
// ...
return &MyCustomError{
message: "...",
details: "...",
}
}