Η Φάρμα

Η Φάρμα

Άσκηση εκμάθησης

Εισαγωγή

Η διεπαφή error

Ο χειρισμός σφαλμάτων δεν γίνεται μέσω εξαιρέσεων στη Go. Αντ' αυτού, τα σφάλματα είναι κανονικές τιμές τύπων που υλοποιούν την ενσωματωμένη διεπαφή error. Η διεπαφή error είναι πολύ λιτή. Περιέχει μόνο μία μέθοδο, τη 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")
}
Caution

Δεν πρέπει να υποθέτεις ότι όλες οι συναρτήσεις επιστρέφουν μηδενικές τιμές για τις άλλες τιμές επιστροφής όταν υπάρχει σφάλμα. Η καλύτερη πρακτική είναι να υποθέτεις ότι δεν είναι ασφαλές να χρησιμοποιήσεις καμία από τις άλλες τιμές επιστροφής αν προέκυψε σφάλμα. Οι μόνες εξαιρέσεις είναι οι περιπτώσεις όπου η τεκμηρίωση δηλώνει ξεκάθαρα ότι άλλες τιμές επιστροφής έχουν νόημα σε περίπτωση σφάλματος.

Αν θέλεις να χρησιμοποιήσεις ένα τέτοιο απλό σφάλμα σε πολλά σημεία, καλό είναι να δηλώσεις μια μεταβλητή για το σφάλμα αντί να χρησιμοποιείς την errors.New απευθείας. Κατά σύμβαση, το όνομα της μεταβλητής πρέπει να ξεκινά με Err ή err (ανάλογα με το αν εξάγεται ή όχι). Αυτές οι μεταβλητές σφάλματος συχνά ονομάζονται sentinel errors.

import "errors"

var ErrNotFound = errors.New("resource was not found")

func DoSomething() error {
  // ...
  return ErrNotFound
}

Επίστρεψε nil για το σφάλμα, για να δηλώσεις ότι δεν υπήρξαν σφάλματα κατά την εκτέλεση της συνάρτησης:

func Foo() (int, error) {
  return 10, nil
}

Έλεγχος σφαλμάτων

Αν καλέσεις μια συνάρτηση που επιστρέφει σφάλμα, συνηθίζεται να αποθηκεύεις την τιμή του σφάλματος σε μια μεταβλητή με όνομα err. Πριν χρησιμοποιήσεις το πραγματικό αποτέλεσμα της συνάρτησης, πρέπει να ελέγξεις ότι δεν υπήρξε σφάλμα.

Για να μην εμφωλευτεί το "happy path" του κώδικά σου, οι περιπτώσεις σφάλματος πρέπει να αντιμετωπίζονται πρώτες. Μπορούμε να χρησιμοποιήσουμε τα == και != για να συγκρίνουμε το σφάλμα με το nil, και ξέρουμε ότι υπήρξε σφάλμα όταν το err δεν είναι nil.

func processUserFile() error {
	file, err := os.Open("./users.csv")
	if err != nil {
		return err
	}

	// do something with file
}

Τις περισσότερες φορές, το σφάλμα θα επιστραφεί προς τα πάνω στη στοίβα των συναρτήσεων, όπως φαίνεται στο παραπάνω παράδειγμα. Ένας άλλος τρόπος να χειριστείς το σφάλμα θα ήταν να το καταγράψεις και να συνεχίσεις με κάποια άλλη ενέργεια. Είναι καλή πρακτική είτε να επιστρέφεις είτε να καταγράφεις το σφάλμα, ποτέ και τα δύο.

Επειδή οι περισσότερες συναρτήσεις στη 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: "...",
  }
}

Οδηγίες

Η μέρα που περίμενες τόσο καιρό ήρθε επιτέλους και τώρα είσαι ο περήφανος ιδιοκτήτης ενός όμορφου αγροκτήματος στις Άλπεις.

Ακόμα δεν σου αρέσει να ξυπνάς πολύ νωρίς το πρωί για να ταΐσεις τις αγελάδες σου. Επειδή είσαι εξαιρετικός μηχανικός, κατασκευάζεις έναν διανομέα τροφής, τον FEED-M-ALL.

Το τελευταίο πράγμα που απαιτείται για να ολοκληρώσεις το έργο σου είναι ένα κομμάτι κώδικα που υπολογίζει την ποσότητα ζωοτροφής που πρέπει να πάρει κάθε αγελάδα. Είναι σημαντικό κάθε αγελάδα να παίρνει την ίδια ποσότητα, πρέπει να αποφύγεις τις συγκρούσεις. Οι αγελάδες είναι πολύ ευαίσθητες.

Ευτυχώς, δεν χρειάζεται να βγάλεις μόνος σου όλους τους τύπους για τον υπολογισμό των ποσοτήτων ζωοτροφής. Χρησιμοποιείς μια μυστηριώδη εξωτερική βιβλιοθήκη που βρήκες στο διαδίκτυο. Υποτίθεται ότι θα κάνει τις αγελάδες όσο πιο ευτυχισμένες γίνεται. Η βιβλιοθήκη εκθέτει έναν τύπο που υλοποιεί την παρακάτω διεπαφή. Θα βασιστείς σε αυτό στον κώδικα που θα γράψεις ο ίδιος.

type FodderCalculator interface {
	FodderAmount(int) (float64, error)
	FatteningFactor() (float64, error)
}

Καθώς δουλεύεις τον κώδικά σου, θα βελτιώσεις τη διαχείριση σφαλμάτων ώστε να είναι πιο ανθεκτική και πιο εύκολη στο debug αργότερα, όταν θα τη χρησιμοποιείς στην καθημερινή ζωή του αγροκτήματος.

1. Μοίρασε την τροφή ομοιόμορφα

Πρώτα απ' όλα, επικεντρώνεσαι στον κώδικα που χρειάζεται για να υπολογίσεις την ποσότητα ζωοτροφής ανά αγελάδα.

Υλοποίησε μια συνάρτηση DivideFood που δέχεται ως ορίσματα ένα FodderCalculator και έναν αριθμό αγελάδων ως ακέραιο. Για αυτή την εργασία, υποθέτεις ότι ο αριθμός των αγελάδων που περνάς είναι πάντα μεγαλύτερος από το μηδέν. Η συνάρτηση πρέπει να επιστρέφει την ποσότητα τροφής ανά αγελάδα ως float64 ή ένα σφάλμα, αν προκύψει κάποιο.

Για να κάνεις τον υπολογισμό, πρώτα πρέπει να ανακτήσεις τη συνολική ποσότητα ζωοτροφής για όλες τις αγελάδες. Αυτό γίνεται καλώντας τη μέθοδο FodderAmount και περνώντας τον αριθμό των αγελάδων. Επιπλέον, χρειάζεσαι έναν παράγοντα με τον οποίο πρέπει να πολλαπλασιαστεί αυτή η ποσότητα. Αυτόν τον παράγοντα τον παίρνεις καλώντας τη μέθοδο FatteningFactor. Με αυτές τις δύο τιμές και τον αριθμό των αγελάδων, μπορείς τώρα να υπολογίσεις την ποσότητα τροφής ανά αγελάδα (ως float64). Αυτό είναι που πρέπει να επιστρέψει η συνάρτηση DivideFood.

Αν μία από τις μεθόδους που καλείς επιστρέψει σφάλμα, η εκτέλεση πρέπει να σταματήσει και αυτό το σφάλμα πρέπει να επιστραφεί (ως έχει) από τη συνάρτηση DivideFood.

// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>

// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"

2. Έλεγξε τον αριθμό των αγελάδων

Ενώ δούλευες την πρώτη εργασία παραπάνω, συνειδητοποίησες ότι η εξωτερική βιβλιοθήκη που χρησιμοποιείς δεν είναι τόσο υψηλής ποιότητας όσο νόμιζες. Για παράδειγμα, δεν μπορεί να χειριστεί σωστά μη έγκυρες εισόδους. Θέλεις να παρακάμψεις αυτόν τον περιορισμό προσθέτοντας έναν έλεγχο για την τιμή εισόδου στον δικό σου κώδικα.

Γράψε μια συνάρτηση ValidateInputAndDivideFood που έχει την ίδια υπογραφή με την DivideFood παραπάνω.

  • Αν ο αριθμός των αγελάδων που περνάς είναι μεγαλύτερος από 0, η συνάρτηση πρέπει να καλέσει τη DivideFood και να επιστρέψει τα αποτελέσματα αυτής της κλήσης.
  • Αν ο αριθμός των αγελάδων είναι 0 ή λιγότερο, η συνάρτηση πρέπει να επιστρέψει ένα σφάλμα με μήνυμα "invalid number of cows".
ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>

ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"

3. Βελτίωσε τη διαχείριση σφαλμάτων

Ο έλεγχος του αριθμού των αγελάδων πριν τον περάσεις παρακάτω ήταν καλή κίνηση, αλλά δεν είσαι και πολύ ευχαριστημένος με το μη συγκεκριμένο μήνυμα σφάλματος. Αποφασίζεις να το κάνεις καλύτερα δημιουργώντας έναν προσαρμοσμένο τύπο σφάλματος, τον InvalidCowsError.

Το προσαρμοσμένο σφάλμα πρέπει να περιέχει τον αριθμό των αγελάδων (int) και ένα προσαρμοσμένο μήνυμα (string), και η μέθοδος Error πρέπει να σειριοποιεί τα δεδομένα στην παρακάτω μορφή:

{number of cows} cows are invalid: {custom message}

Εφοδιασμένος με το προσαρμοσμένο σφάλμα σου, υλοποίησε μια συνάρτηση ValidateNumberOfCows που δέχεται τον αριθμό των αγελάδων ως ακέραιο και επιστρέφει ένα σφάλμα (ή nil).

  • Αν ο αριθμός των αγελάδων είναι μικρότερος από 0, η συνάρτηση επιστρέφει ένα InvalidCowsError με το προσαρμοσμένο μήνυμα ορισμένο σε "there are no negative cows".
  • Αν ο αριθμός των αγελάδων είναι 0, η συνάρτηση επιστρέφει ένα InvalidCowsError με το προσαρμοσμένο μήνυμα ορισμένο σε "no cows don't need food".
  • Διαφορετικά, η συνάρτηση επιστρέφει nil για να δείξει ότι η επικύρωση πέτυχε.
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

Μετά τη σκληρή δουλειά της δημιουργίας αυτής της συνάρτησης επικύρωσης, παρατηρείς ότι έχει ήδη βραδιάσει και αφήνεις το γραφείο σου για να απολαύσεις το ηλιοβασίλεμα πάνω από τα βουνά. Τη δουλειά να προσθέσεις πραγματικά τη νέα συνάρτηση επικύρωσης στον κώδικά σου την αφήνεις για άλλη μέρα.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
Go Exercism

Έτοιμος να ξεκινήσεις την άσκηση Η Φάρμα;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις Go με 34 έννοιες166 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.