web-dev-qa-db-fra.com

Un package Go doit-il jamais utiliser log.Fatal et quand?

J'ai jusqu'à présent évité l'utilisation de log.Fatal, mais j'ai récemment découvert par hasard ces questions; couverture de code et tests-using-log-fatal .

L'un des commentaires des 100 questions de couverture de code dit:

... Dans la grande majorité des cas log.Fatal ne doit être utilisé que dans les fonctions main ou init (ou peut-être certaines choses qui ne doivent être appelées que directement à partir de celles-ci) "

Cela me fait réfléchir, alors j'ai commencé à regarder le code de bibliothèque standard fourni avec Go. Il existe de nombreux exemples où le code de test de la bibliothèque utilise log.Fatal ce qui semble bien. Il y a quelques exemples en dehors du code de test, comme dans net/http , illustré ci-dessous:

// net/http/transport.go 
func (t *Transport) putIdleConn(pconn *persistConn) bool {
    ...
    for _, exist := range t.idleConn[key] {
        if exist == pconn {
            log.Fatalf("dup idle pconn %p in freelist", pconn)
        }
    }
    ...
}

S'il est préférable d'éviter d'utiliser log.Fatal, pourquoi est-il utilisé dans les bibliothèques standard, je me serais attendu à ce que je retourne une erreur. Il semble injuste pour l'utilisateur de la bibliothèque de provoquer os.Exit à appeler et ne donnant aucune chance au nettoyage de l'application.

Je peux être naïf, donc ma question comme meilleure pratique semble être d'appeler log.Panic qui peut être récupéré et mon application stable théorique à long terme pourrait avoir une chance de renaître de ses cendres.

Alors, que dirait la meilleure pratique pour Go à propos du moment où la connexion devrait être utilisée?

26
miltonb

C'est peut-être juste moi, mais voici comment j'utilise log.Fatal. Conformément aux conventions UNIX, un processus qui rencontre une erreur doit échouer le plus tôt possible avec un code de sortie différent de zéro. Cela m'a amené aux directives suivantes pour utiliser log.Fatal Lorsque…

  1. … Une erreur se produit dans l'une de mes func init(), car celles-ci se produisent respectivement lors du traitement des importations ou avant l'appel de la fonction principale. Inversement, je ne fais que des choses n'affectant pas directement l'unité de travail que la bibliothèque ou cmd est censée faire. Par exemple, je configure la journalisation et vérifie si nous avons un environnement et des paramètres sains. Pas besoin d'exécuter main si nous avons des drapeaux invalides, non? Et si nous ne pouvons pas donner une rétroaction appropriée, nous devrions le dire tôt.
  2. … Une erreur se produit dont je sais qu'elle est irrécupérable. Supposons que nous ayons un programme qui crée une miniature d'un fichier image donné sur la ligne de commande. Si ce fichier n'existe pas ou est illisible en raison d'autorisations insuffisantes, il n'y a aucune raison de continuer et cette erreur ne peut pas être récupérée. Nous adhérons donc aux conventions et échouons.
  3. … Une erreur se produit lors d'un processus qui peut ne pas être réversible. C'est une sorte de définition douce, je sais. Permettez-moi d'illustrer cela. Supposons que nous ayons une implémentation de cp, et qu'elle a commencé à être non interactive et à copier récursivement un répertoire. Supposons maintenant que nous rencontrons un fichier dans le répertoire cible qui porte le même nom (mais un contenu différent) qu'un fichier à y copier. Comme nous ne pouvons pas demander à l'utilisateur de décider quoi faire et que nous ne pouvons pas copier ce fichier, nous avons un problème. Parce que l'utilisateur supposera que les répertoires source et cible sont des copies exactes lorsque nous aurons terminé avec le code de sortie zéro, nous ne pouvons pas simplement ignorer le fichier en question. Cependant, nous ne pouvons pas simplement l'écraser, car cela pourrait potentiellement détruire des informations. Il s'agit d'une situation que nous ne pouvons pas récupérer par demande explicite de l'utilisateur, et j'utiliserais donc log.Fatal Pour expliquer la situation, obéissant ainsi au principe de l'échec le plus tôt possible.
32
Markus W Mahlberg

Marcus, je suis tombé sur votre réponse, et je pense qu'elle est excellente et très perspicace, et j'ai tendance à être d'accord avec votre ventilation. Il est très difficile de généraliser, même si j'y ai réfléchi un peu plus et en tant que débutant. Je pense que sur le plan théorique, si nous recherchons les meilleures pratiques en informatique, indépendamment du système d'exploitation, du framework de package ou de la bibliothèque, la responsabilité d'un enregistreur est de simplement se connecter. À tous les niveaux, les responsabilités d'un enregistreur:

  • Formatez et imprimez les informations de manière cohérente sur le canal de mon choix.
  • Catégoriser, filtrer, afficher les différents niveaux de journalisation [débogage, info, avertir, erreur]
  • Gérer et agréger les entrées de journal sur des tâches asynchrones et parallèles

Un package de journalisation ou tout autre package n'a pas et ne devrait pas avoir le droit de planter un programme s'il fonctionne correctement. Tout middleware ou bibliothèque doit suivre un modèle throw/catch, avec la possibilité pour toutes les exceptions levées d'être interceptées par l'appelant. C'est également un bon modèle à suivre dans une application, car lorsque vous construisez des fondations et des packages qui alimentent diverses parties de votre application, et potentiellement d'autres applications, ils ne doivent jamais planter une application directement. Au lieu de cela, ils devraient lever une exception fatale, permettant au programme de gérer. Je pense que cela répond également à certains de vos points Marcus, car cela fonctionne pour alerter l'appelant immédiatement lorsqu'il n'est pas pris, comme un crash fatal.

Dans la plupart des cas, je peux profiter de l'exploitation de log. Je ne pense pas que cela ait du sens en tant qu'approche à long terme pour gérer les erreurs fatales dans les packages.

1
moniecodes