Bonjour à tous, je suis poisson frit.

Il y a quelque temps j'ai partagé un article " 10+ Proverbes officiels de Go, combien en connaissez-vous ? , qui a suscité des discussions entre de nombreux amis. L'un d'eux est "Les erreurs sont des valeurs". Tout le monde saute à plusieurs reprises entre "Les erreurs sont des valeurs" ou "Les erreurs sont des valeurs", ce qui n'est pas facile à emmêler.

En fait, Rob Pike, qui a dit cette phrase, a utilisé un article " Les erreurs sont des valeurs [1] " pour interpréter le sens de ce proverbe. Que puis-je faire d'autre?

Aujourd'hui, je vais apprendre le poisson frit avec tout le monde. Les "je" suivants représentent tous Rob Pike, et certaines vues sont complétées par du poisson frit.

Il est fortement recommandé de le lire attentivement.

Contexte

L'une des choses dont les programmeurs Go, en particulier ceux qui découvrent le langage, parlent beaucoup est de savoir comment gérer les erreurs. Pour le nombre de fois que l'extrait de code suivant apparaît, la conversation se transforme souvent en une complainte.

(Les grandes plateformes se plaignent et critiquent beaucoup, pensant que le design n'est pas bon, etc.)

Le code suivant :

if err != nil {
    return err
}

Scanner les extraits de code

Nous avons récemment scanné tous les projets open source Go que nous avons pu trouver et avons constaté que cet extrait n'apparaît qu'une fois toutes les pages ou deux, ce qui est moins que certaines personnes ne le pensent.

Néanmoins, si les gens ont encore l'impression qu'ils doivent souvent entrer du code comme celui-ci :

if err != nil

Alors il doit y avoir quelque chose qui ne va pas, et la cible évidente est le langage Go lui-même (mal conçu ?).

mauvaise compréhension

Évidemment, c'est malheureux, trompeur et facile à corriger. C'est peut-être le cas maintenant que les programmeurs novices en Go demanderont : "Comment gérez-vous les erreurs ?", apprenez ce modèle if err !=nil, et arrêtez-vous là.

Dans d'autres langages, on peut utiliser des blocs try-catch ou d'autres mécanismes similaires pour gérer les erreurs. Tant de programmeurs pensent que lorsque j'utiliserais try-catch dans les langages précédents, je tape simplement if err != nil dans Go.

Au fil du temps, le code Go collecte de nombreux fragments if err != nil, et le résultat semble difficile à manier.

l'erreur est la valeur

Que cette interprétation soit appropriée ou non, il est clair que ces programmeurs Go passent à côté d'un point fondamental sur les erreurs : les erreurs sont des valeurs .

Les valeurs peuvent être programmées, et puisque les erreurs sont des valeurs, les erreurs peuvent également être programmées.

Bien sûr, une déclaration courante impliquant une valeur d'erreur consiste à tester si elle est nulle, mais il existe d'innombrables autres choses que vous pouvez faire avec une valeur d'erreur, et l'application de certaines de ces autres choses peut améliorer votre programme, en supprimant beaucoup de if erreur !=néant passe-partout .

La situation mentionnée ci-dessus se produit si vous utilisez une instruction par cœur si pour vérifier chaque erreur.

exemple bufio

Vous trouverez ci-dessous un exemple simple du type Scanner du package bufio. Sa méthode Scan effectue des E/S de bas niveau, ce qui provoque bien sûr une erreur. Cependant, la méthode Scan ne présente aucune erreur.

Au lieu de cela, il renvoie un booléen et exécute une méthode distincte à la fin de l'analyse signalant si une erreur s'est produite.

Le code client ressemble à ceci :

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}

Bien sûr, il y a une erreur de vérification nulle, mais elle n'apparaît et ne s'exécute qu'une seule fois. La méthode Scan pourrait plutôt être définie comme :

func (s *Scanner) Scan() (token []byte, error)

Ensuite, un exemple de code utilisateur pourrait être (selon la façon dont le jeton est récupéré) :

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

Ce n'est pas si différent, mais il y a une différence importante. Dans ce code, le client doit vérifier les erreurs à chaque itération, mais dans la véritable API du scanner, la gestion des erreurs est abstraite de l'élément clé de l'API, qui itère sur les jetons.

Avec une vraie API, le code côté client semble donc plus naturel : bouclez jusqu'à ce que vous ayez terminé, puis souciez-vous des bogues.

La gestion des erreurs n'obscurcit pas le flux de contrôle.

Bien sûr, ce qui se passe dans les coulisses, c'est qu'une fois que Scan rencontre une erreur d'E/S, il l'enregistre et renvoie false. Une méthode distincte Err signale la valeur d'erreur lorsque le client le lui demande.

Bien que ce soit trivial, ce n'est pas la même chose que de jeter if err != nilaprès ou de demander au client de vérifier les erreurs. C'est de la programmation avec des valeurs erronées. Programmation simple, oui, mais toujours programmation.

Il convient de souligner que, quelle que soit la conception, il est essentiel que les programmes vérifient les erreurs, peu importe où elles sont exposées. La discussion ici ne porte pas sur la façon d'éviter de vérifier les erreurs, mais sur l'utilisation du langage pour gérer les erreurs avec élégance.

Discussion pratique

Le sujet de la duplication du code de vérification des erreurs a été soulevé lorsque j'ai assisté à la GoCon d'automne 2014 à Tokyo. Un Gopher enthousiaste, @jxck_ sur Twitter, a répondu à la plainte familière sur la vérification des erreurs.

Il a un code, qui est structuré comme ceci :

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

C'est très répétitif. Dans le code réel, ce code est plus long et a plus de choses à faire, il n'est donc pas facile de simplement refactoriser ce code avec une fonction d'assistance, mais sous cette forme idéalisée, une fonction ferme littéralement Helpful pour les variables d'erreur :

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

Ce modèle fonctionne bien, mais doit être désactivé dans chaque fonction qui effectue l'écriture ; les fonctions d'assistance séparées sont difficiles à utiliser car la variable err doit être maintenue entre les appels (essayez-le).

Nous pouvons le rendre plus simple, plus général et plus réutilisable en empruntant des idées à la méthode de numérisation ci-dessus. J'ai mentionné cette technique dans notre discussion, mais @jxck_ n'a pas vu comment l'appliquer.

Après une longue période de communication, j'ai demandé si je pouvais emprunter son cahier et taper quelques codes pour qu'il les voie à cause de la barrière de la langue.

Je définis un objet appelé errWriter comme ceci :

type errWriter struct {
    w   io.Writer
    err error
}

Et a écrit une méthode : Write. Il n'a pas besoin d'avoir la signature Write standard et est partiellement en minuscules pour mettre en évidence la différence.

La méthode write appelle la méthode Writer sous-jacente et enregistre la première erreur pour référence :

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

Lorsqu'une erreur se produit, la méthode Write devient inutile, mais la valeur d'erreur est enregistrée.

Étant donné le type errWriter et sa méthode Write, le code ci-dessus peut être refactorisé dans le code suivant :

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

C'est plus propre et rend même l'ordre d'écriture réel plus facile à voir sur la page que d'utiliser une fermeture. Plus de confusion. La programmation avec des valeurs d'erreur (et des interfaces) améliore le code.

Très probablement, un autre code du même package pourrait s'appuyer sur cette idée et même utiliser directement errWriter.

De plus, une fois que errWriter existe, il peut faire beaucoup plus pour aider, en particulier dans les exemples moins conviviaux. Il peut accumuler des octets. Il peut condenser les écritures dans un tampon et les transférer de manière atomique. il y en a plus.

En fait, ce motif apparaît fréquemment dans la bibliothèque standard. Il est utilisé par les packages archive/zip et net/http. Ce qui est plus important dans cette discussion, c'est que le Writer du paquet bufio est en fait une implémentation de l'idée de errWriter. Bien que bufio.Writer.Write renvoie une erreur, c'est principalement pour respecter l'interface io.Writer.

La méthode Write de bufio.Writer se comporte comme notre méthode errWriter.write ci-dessus, Flush signalera une erreur, donc notre exemple peut être écrit comme ceci :

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}

Cette approche présente un inconvénient évident, du moins pour certaines applications : il n'y a aucun moyen de savoir combien de traitement est effectué avant que l'erreur ne se produise. Si ces informations sont importantes, une approche plus fine est nécessaire. Habituellement, cependant, une vérification tout ou rien à la fin est suffisante.

Résumer

Dans cet article, nous avons uniquement examiné une technique pour éviter de dupliquer le code de gestion des erreurs.

Gardez à l'esprit qu'utiliser errWriter ou bufio.Writer n'est pas le seul moyen de simplifier la gestion des erreurs, et cela ne fonctionnera pas dans toutes les situations.

Cependant, la leçon clé est que les erreurs sont des valeurs et que toute la puissance du langage de programmation Go est disponible pour les traiter .

Utilisez ce langage pour simplifier votre gestion des erreurs.

Mais rappelez-vous : quoi que vous fassiez, vérifiez vos erreurs !

Références

[1]

Les erreurs sont des valeurs : https://go.dev/blog/errors-are-values

Suivez et ajoutez Fried Fish WeChat,

Obtenez des nouvelles et des connaissances de première main sur l'industrie et intégrez-vous au groupe d' échange👇

image

image

Bonjour, je suis Jianyu. J'ai publié le best-seller Go "The Journey of Go Language Programming", puis j'ai remporté l'honneur du GOP (l'expert le plus avisé dans le domaine du Go). Cliquez sur le mot bleu pour voir ma route d'édition de livre .

Partagez quotidiennement des articles de haute qualité, produisez des interviews Go, une expérience de travail, une conception d'architecture et rejoignez WeChat pour attirer les lecteurs vers le groupe d'échange afin de communiquer avec tout le monde !