108 votes

Évitez de vérifier si l'erreur est nulle ?

Je suis actuellement en train d'apprendre go et une partie de mon code ressemble à ceci :

a, err := doA()
if err != nil {
  return nil, err
}
b, err := doB(a)
if err != nil {
  return nil, err
}
c, err := doC(b)
if err != nil {
  return nil, err
}
... et ainsi de suite ...

Cela me semble un peu faux car la vérification des erreurs prend la plupart des lignes. Existe-t-il un moyen plus efficace de gérer les erreurs ? Serait-il possible d'éviter cela en refactorisant ?

MISE À JOUR : Merci pour toutes les réponses. Veuillez noter que dans mon exemple, doB dépend de a, doC dépend de b, et ainsi de suite. Par conséquent, la plupart des refontes suggérées ne fonctionnent pas dans ce cas. Une autre suggestion ?

61voto

Gustavo Niemeyer Points 4759

C'est une plainte courante, et il existe plusieurs réponses à cela.

Voici quelques-unes des plus courantes :

1 - Ce n'est pas si mal

C'est une réaction très courante à ces plaintes. Le fait d'avoir quelques lignes de code supplémentaires dans votre code n'est en fait pas si mal. C'est juste un peu de frappe bon marché, et très facile à gérer du côté de la lecture.

2 - C'est en fait une bonne chose

Cela repose sur le fait que taper et lire ces lignes supplémentaires est un très bon rappel que en fait votre logique pourrait s'échapper à ce moment-là, et vous devez annuler toute gestion des ressources que vous avez mise en place dans les lignes précédentes. Cela est généralement évoqué en comparaison avec les exceptions, qui peuvent interrompre le flux logique de manière implicite, obligeant le développeur à avoir toujours en tête le chemin d'erreur caché. Il y a quelque temps j'ai écrit une critique plus approfondie à ce sujet ici.

3 - Utiliser panic/recover

Dans certaines circonstances spécifiques, vous pouvez éviter une partie de ce travail en utilisant panic avec un type connu, puis en utilisant recover juste avant que votre code de package ne soit diffusé, le transformant en une erreur correcte et la retournant à la place. Cette technique est le plus souvent utilisée pour dérouler la logique récursive telle que les (dé)sérialiseurs.

Personnellement, j'essaie de ne pas abuser de cela, car je suis plus proche des points 1 et 2.

4 - Réorganiser un peu le code

Dans certaines circonstances, vous pouvez réorganiser légèrement la logique pour éviter la répétition.

Comme exemple trivial, ceci :

err := doA()
if err != nil {
    return err
}
err := doB()
if err != nil {
    return err
}
return nil

peut aussi être organisé comme suit :

err := doA()
if err != nil {
    return err
}
return doB()

5 - Utiliser des résultats nommés

Certaines personnes utilisent des résultats nommés pour supprimer la variable err de l'instruction de retour. Je déconseillerais de le faire, cependant, car cela économise très peu, réduit la clarté du code, et rend la logique sujette à des problèmes subtils lorsque un ou plusieurs résultats sont définis avant l'instruction de retour de sortie.

6 - Utiliser la déclaration avant la condition if

Comme Tom Wilde l'a bien rappelé dans le commentaire ci-dessous, les instructions if en Go acceptent une simple déclaration avant la condition. Vous pouvez donc faire ceci :

if err := doA(); err != nil {
    return err
}

C'est un bel idiome Go, et souvent utilisé.

Dans certains cas spécifiques, je préfère éviter d'intégrer la déclaration de cette manière juste pour qu'elle se démarque par sa clarté, mais c'est une chose subtile et personnelle.

18voto

nemo Points 13983

Réponse de 2023

De nos jours, vous utiliseriez des helpers du package errors tels que errors.Join pour gérer ces situations :

x, err1 := doSomething(2)
y, err2 := doSomething(3)

if err := errors.Join(err1, err2); err != nil {
    return err
}

Si vous souhaitez filtrer des erreurs spécifiques que vous souhaitez gérer séparément (au lieu de simplement les enregistrer et de faire une gestion générale des erreurs), vous pouvez utiliser errors.As ou errors.Is :

func foo() error {
    x, err1 := doSomething(2)
    y, err2 := doSomething(3)

    if err := errors.Join(err1, err2); err != nil {
        return err
    }
}

err := foo()

if error.Is(err, fs.ErrNotExist) {
    // gérer le fichier non trouvé en, par exemple, affichant
    // un message spécial mais l'erreur concrète ne peut pas être
    // accédée directement, consultez `.As` ci-dessous pour cela
}

var notExistErr *fs.ErrNotExist
if error.As(err, notExistErr) {
    // atteint uniquement si l'assignation est possible
    // maintenant `notExistErr` peut être utilisé pour extraire des infos sur err
}

Réponse de 2013

Si vous avez de nombreuses situations récurrentes où vous avez plusieurs de ces vérifications d'erreur, vous pouvez vous définir une fonction utilitaire comme suit :

func validError(errs ...error) error {
    for i, _ := range errs {
        if errs[i] != nil {
            return errs[i]
        }
    }
    return nil
}

Cela vous permet de sélectionner l'une des erreurs et de la renvoyer s'il y en a une qui n'est pas nulle.

Exemple d'utilisation (version complète sur play) :

x, err1 := doSomething(2)
y, err2 := doSomething(3)

if e := validError(err1, err2); e != nil {
    return e
}

Évidemment, cela ne peut s'appliquer que si les fonctions ne dépendent pas les unes des autres, mais c'est une condition préalable générale à la gestion des erreurs.

8voto

Nick Craig-Wood Points 18742

Vous pourriez utiliser des paramètres de retour nommés pour raccourcir un peu les choses

Lien Playground

func doStuff() (result string, err error) {
    a, err := doA()
    if err != nil {
        return
    }
    b, err := doB(a)
    if err != nil {
        return
    }
    result, err = doC(b)
    if err != nil {
        return
    }
    return
}

Après avoir programmé en Go pendant un certain temps, vous apprécierez le fait de devoir vérifier l'erreur pour chaque fonction vous oblige à réfléchir à ce que cela signifie réellement si cette fonction échoue et comment vous devriez y faire face.

3voto

zouying Points 431

Vous pourriez créer un type de contexte avec une valeur de résultat et une erreur.

type Type1 struct {
    a int
    b int
    c int

    err error
}

func (t *Type1) doA() {
    if t.err != nil {
        return
    }

    // faire quelque chose
    if err := do(); err != nil {
        t.err = err
    }
}

func (t *Type1) doB() {
    if t.err != nil {
        return
    }

    // faire quelque chose
    b, err := t.doWithA(a)
    if err != nil {
        t.err = err
        return
    }

    t.b = b
}

func (t *Type1) doC() {
    if t.err != nil {
        return
    }

    // faire quelque chose
    c, err := do()
    if err != nil {
        t.err = err
        return
    }

    t.c = c
}

func main() {

    t := Type1{}
    t.doA()
    t.doB()
    t.doC()

    if t.err != nil {
        // gérer l'erreur dans t
    }

}

1voto

robert king Points 5369

Vous pouvez transmettre une erreur en tant qu'argument de fonction

func doA() (A, error) {
...
}
func doB(a A, err error)  (B, error) {
...
} 

c, err := doB(doA())

J'ai remarqué que certaines méthodes du package "html/template" font cela par exemple

func Must(t *Template, err error) *Template {
    if err != nil {
        panic(err)
    }
    return t
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X