La ferme

La ferme

Exercice d'apprentissage

Introduction

L'interface error

En Go, la gestion des erreurs ne se fait pas au moyen d'exceptions. À la place, les erreurs sont des valeurs ordinaires de types qui implémentent l'interface error intégrée. L'interface error est très minimale. Elle ne contient qu'une seule méthode Error() qui renvoie le message d'erreur sous forme de string.

type error interface {
  Error() string
}

Chaque fois que tu définis une fonction dans laquelle une erreur peut survenir pendant l'exécution et doit remonter jusqu'à l'appelant, tu dois inclure error parmi les types de retour. Si la fonction prend plusieurs valeurs de retour, par convention error est toujours la dernière.

func DoSomething() (int, error) {
  // ...
}

Crée et renvoie une erreur simple

Tu n'as pas toujours besoin d'implémenter toi-même l'interface error. Pour créer une erreur simple, tu peux utiliser la fonction errors.New() qui fait partie du paquet errors de la bibliothèque standard. La seule chose à passer est le message d'erreur sous forme de string, et errors.New() se charge de créer une valeur qui contient ton message et qui implémente l'interface error.

Si la fonction renvoie une erreur, la bonne pratique est de renvoyer la valeur zéro pour tous les autres paramètres de retour :

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

Tu ne dois pas supposer que toutes les fonctions renvoient des valeurs zéro pour les autres valeurs de retour lorsqu'une erreur est présente. La meilleure pratique consiste à considérer qu'il n'est pas sûr d'utiliser les autres valeurs de retour si une erreur s'est produite. Les seules exceptions sont les cas où la documentation indique clairement que les autres valeurs de retour sont significatives en cas d'erreur.

Si tu veux utiliser une telle erreur simple à plusieurs endroits, il vaut mieux déclarer une variable pour l'erreur plutôt que d'utiliser errors.New en ligne. Par convention, le nom de cette variable doit commencer par Err ou err (selon qu'elle est exportée ou non). Ces variables d'erreur sont souvent appelées erreurs sentinelles.

import "errors"

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

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

Renvoie nil pour l'erreur afin de signaler qu'il n'y a pas eu d'erreur pendant l'exécution de la fonction :

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

Vérification des erreurs

Si tu appelles une fonction qui renvoie une erreur, il est courant de stocker la valeur de l'erreur dans une variable appelée err. Avant d'utiliser le résultat réel de la fonction, tu dois vérifier qu'il n'y a pas eu d'erreur.

Pour éviter d'imbriquer le « happy path » de ton code, il faut traiter les cas d'erreur en premier. On peut utiliser == et != pour comparer l'erreur à nil, et on sait qu'il y a eu une erreur quand err n'est pas nil.

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

	// do something with file
}

La plupart du temps, l'erreur remonte la pile d'appels, comme le montre l'exemple ci-dessus. Une autre façon de traiter l'erreur consiste à la journaliser et à poursuivre avec une autre opération. La bonne pratique est de renvoyer ou de journaliser l'erreur, jamais les deux.

Comme la plupart des fonctions en Go incluent une erreur parmi les valeurs de retour, tu verras et utiliseras le motif if err != nil un peu partout dans le code Go.

Types d'erreur personnalisés

Si tu veux que ton erreur contienne plus d'informations que le simple message d'erreur, tu peux créer un type d'erreur personnalisé. Comme mentionné plus haut, tout ce qui implémente l'interface error (c'est-à-dire qui possède une méthode Error() string) peut servir d'erreur en Go.

En général, on utilise une structure pour créer un type d'erreur personnalisé. Par convention, les noms des types d'erreur personnalisés doivent se terminer par Error. Il est aussi préférable de définir la méthode Error() string avec un récepteur pointeur ; consulte ce commentaire Stack Overflow pour comprendre le raisonnement. Note que cela signifie que tu dois renvoyer un pointeur vers ton erreur personnalisée, sinon elle ne sera pas considérée comme une error, car la valeur non-pointeur ne fournit pas la méthode 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: "...",
  }
}

Instructions

Le jour que tu attendais depuis si longtemps est enfin arrivé : tu es maintenant le fier propriétaire d'une magnifique ferme dans les Alpes.

Tu n'aimes toujours pas te lever trop tôt le matin pour nourrir tes vaches. Comme tu es un excellent ingénieur, tu construis un distributeur de nourriture, le FEED-M-ALL.

La dernière chose qu'il te reste à faire pour terminer ton projet est un bout de code qui calcule la quantité de fourrage que chaque vache doit recevoir. Il est important que chaque vache reçoive la même quantité, tu dois éviter les conflits. Les vaches sont très sensibles.

Heureusement, tu n'as pas besoin de trouver toi-même toutes les formules pour calculer les quantités de fourrage. Tu utilises une mystérieuse bibliothèque externe que tu as trouvée sur internet. Elle est censée produire les vaches les plus heureuses. La bibliothèque expose un type qui satisfait l'interface suivante. C'est sur cela que tu t'appuieras dans le code que tu écriras toi-même.

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

Au fur et à mesure que tu travailles sur ton code, tu amélioreras la gestion des erreurs pour la rendre plus robuste et plus facile à déboguer plus tard, quand tu l'utiliseras dans ta vie quotidienne à la ferme.

1. Répartis la nourriture équitablement

Tout d'abord, concentre-toi sur l'écriture du code nécessaire pour calculer la quantité de fourrage par vache.

Implémente une fonction DivideFood qui prend un FodderCalculator et un nombre de vaches sous forme d'entier comme arguments. Pour cette tâche, tu considères que le nombre de vaches passé est toujours supérieur à zéro. La fonction doit renvoyer la quantité de nourriture par vache sous la forme d'un float64, ou une erreur le cas échéant.

Pour faire le calcul, tu dois d'abord récupérer la quantité totale de fourrage pour toutes les vaches. Cela se fait en appelant la méthode FodderAmount et en lui passant le nombre de vaches. De plus, tu as besoin d'un facteur par lequel cette quantité doit être multipliée. Tu obtiens ce facteur en appelant la méthode FatteningFactor. Avec ces deux valeurs et le nombre de vaches, tu peux maintenant calculer la quantité de nourriture par vache (sous forme de float64). C'est ce que la fonction DivideFood doit renvoyer.

Si l'une des méthodes que tu appelles renvoie une erreur, l'exécution doit s'arrêter et cette erreur doit être renvoyée (telle quelle) par la fonction 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. Vérifie le nombre de vaches

En travaillant sur la première tâche ci-dessus, tu t'es rendu compte que la bibliothèque externe que tu utilises n'est pas d'aussi bonne qualité que tu le pensais. Par exemple, elle ne sait pas gérer correctement les entrées invalides. Tu veux contourner cette limitation en ajoutant une vérification de la valeur d'entrée dans ton propre code.

Écris une fonction ValidateInputAndDivideFood qui a la même signature que DivideFood ci-dessus.

  • Si le nombre de vaches passé est supérieur à 0, la fonction doit appeler DivideFood et renvoyer le résultat de cet appel.
  • Si le nombre de vaches est inférieur ou égal à 0, la fonction doit renvoyer une erreur avec le message "invalid number of cows".
ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>

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

3. Améliore la gestion des erreurs

Vérifier le nombre de vaches avant de le transmettre était une bonne idée, mais le message d'erreur imprécis ne te satisfait pas vraiment. Tu décides de faire mieux en créant un type d'erreur personnalisé appelé InvalidCowsError.

L'erreur personnalisée doit contenir le nombre de vaches (int) et un message personnalisé (string), et la méthode Error doit sérialiser les données au format suivant :

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

Maintenant que tu as ton erreur personnalisée, implémente une fonction ValidateNumberOfCows qui prend le nombre de vaches sous forme d'entier et renvoie une erreur (ou nil).

  • Si le nombre de vaches est inférieur à 0, la fonction renvoie une InvalidCowsError avec le message personnalisé défini sur "there are no negative cows".
  • Si le nombre de vaches est 0, la fonction renvoie une InvalidCowsError avec le message personnalisé défini sur "no cows don't need food".
  • Sinon, la fonction renvoie nil pour indiquer que la validation a réussi.
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

Après le dur labeur que représente la mise en place de cette fonction de validation, tu remarques que c'est déjà le soir et tu quittes ton bureau pour admirer le coucher de soleil sur les montagnes. Tu remets à un autre jour la tâche d'ajouter réellement la nouvelle fonction de validation à ton code.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Go Exercism

Prêt à commencer La ferme ?

Inscris-toi sur Exercism pour apprendre et maîtriser Go avec 34 concepts165 exercices, et un vrai mentorat humain, le tout gratuitement.