Stas Afanasyev. Juno. Pipelines basés sur io.Reader / io.Writer. Partie 2

Dans le rapport, nous parlerons du concept de io.Reader / io.Writer, pourquoi ils sont nécessaires, comment les implémenter correctement et quels pièges existent à cet égard, ainsi que de la construction de pipelines basés sur des implémentations standard et personnalisées de io.Reader / io.Writer .



Stas Afanasyev. Juno. Pipelines basés sur io.Reader / io.Writer. Partie 1

Bug «on trust»


Autre nuance: dans cette implémentation il y a un «bagul». Ce bug est confirmé par les développeurs (je leur ai écrit à ce sujet). Peut-être que quelqu'un sait ce qu'est ce «bagul»? Sur la diapositive, c'est l'avant-dernière ligne:



elle est associée à une trop grande confiance dans le Reader encapsulé: si Reader retourne un nombre négatif d'octets, alors la limite que nous aimerions obtenir par le nombre d'octets soustraits augmente. Et dans certains cas, il s'agit d'un bug assez grave que vous ne pouvez pas comprendre immédiatement.

J'ai écrit dans le numéro: faisons quelque chose, réparons-le! Et puis une couche de problèmes a révélé ... D'abord, ils m'ont dit que si vous ajoutez ce chèque maintenant ici, vous devrez l'ajouter partout, et il y a une douzaine de ces endroits. Si nous voulons déplacer cela du côté client, nous devons déterminer un certain nombre de règles selon lesquelles le client validera les données (et il peut également y en avoir cinq ou deux). Il s'avère que tout cela doit être copié.

Je suis d'accord que ce n'est pas optimal. Venons-en alors à une version cohérente! Pourquoi avons-nous une implémentation de la bibliothèque standard qui ne fait confiance à rien, alors que d'autres font confiance à tout?

En général, pendant que j'écrivais mon opinion civique, en y réfléchissant, nous avons clos le problème avec des commentaires - «Nous ne ferons rien. Au revoir"! Ils m'ont fait ressembler à une sorte de fou ... poliment, bien sûr, vous ne pouvez pas trouver à redire.

En général, nous avons maintenant un problème. Il consiste en ce qu'il n'est pas clair qui devrait valider les données du Reader encapsulé. Soit le client, soit nous faisons entièrement confiance au contrat ... Nous avons une solution! S'il reste du temps, j'en parlerai.

Passons au cas suivant.

Teereader


Nous avons examiné un exemple de la façon d'encapsuler les données du Reader. L'exemple suivant de canaux consiste à dépasser les données Reader dans Writer. Il y a deux situations.

Première situation. Nous devons lire les données de Reader, les copier d'une manière ou d'une autre sur Writer (de manière transparente) et les utiliser comme avec Reader. Il existe une implémentation de TeeReader pour cela. Il est présenté dans l'extrait de mise en œuvre supérieur:



fonctionne comme l'équipe Tee sur Unix. Je pense que beaucoup d'entre vous en ont entendu parler.
Notez que cette implémentation vérifie le nombre d'octets qu'elle lit à partir du Reader encapsulé. Voir les conditions dans la deuxième ligne? Parce que lorsque vous écrivez une telle implémentation, c'est intuitivement clair: en cas de nombre négatif, vous aurez la panique. Et c'est un autre endroit où nous faisons confiance au Reader emballé! Je vous rappelle que ce sont toutes des bibliothèques standard.

Passons à un cas, par exemple, comment l'utiliser. Que ferons-nous sur l'extrait de code inférieur? Nous téléchargerons le fichier robot.txt depuis golang.org en utilisant le client http standard.

Comme vous le savez, le client http nous renvoie une structure de réponse, dans laquelle le champ Body est une implémentation de l'interface Reader. Il convient de le préciser en disant qu'il s'agit d'une implémentation de l'interface ReadCloser. Mais ReadCloser n'est qu'une interface construite à partir de Reader et Closer. Autrement dit, il s'agit d'un lecteur, qui peut, en général, être fermé.

Dans cet exemple (dans l'extrait de code inférieur), nous collectons TeeReader, qui va lire les données de ce corps et les écrire dans un fichier. La création du dossier aujourd'hui, malheureusement, est restée en coulisses, car tout ne convenait pas. Mais, encore une fois, si vous regardez le dendrogramme, le type de fichier implémente l'interface Writer, c'est-à-dire que nous pouvons y écrire. Il est évident.

Nous avons assemblé notre TeeReader et l'avons lu à l'aide de ReadAll. Tout fonctionne comme prévu: nous soustrayons le corps résultant, l'écrivons dans un fichier et le voyons dans Assad out.

Voie débutant


La deuxième situation. Nous avons juste besoin de lire les données de Reader et de les écrire dans Writer. La solution est évidente ...

Quand je viens de commencer à travailler avec Go, j'ai résolu des problèmes comme sur une diapositive:



j'ai localisé le tampon, l'ai rempli avec les données de Reader et transféré la tranche remplie vers Writer. Tout est simple.

Deux points. Tout d'abord, il n'y a aucune garantie que l'intégralité du Reader sera soustraite en un seul appel à la méthode Read, car il peut y avoir des données (dans le bon sens, cela devrait être fait en boucle).

Le deuxième point est que ce chemin n'est pas optimal. Voici un joli code passe-partout qui est écrit devant nous.

Pour cela, il existe une famille spéciale d'aides dans la bibliothèque standard - ce sont Copy, CopyN et CopyBuffer.

io.Copie. WriterTo et ReaderFrom


io.Copy fait essentiellement ce qu'il était sur la diapositive précédente: il alloue un tampon par défaut de 32 Ko et écrit des données de Reader à Writer (la signature de cette copie est affichée sur l'extrait supérieur):



en plus de cette routine de modèle, elle contient également une série d'optimisations délicates. Et avant de parler de ces optimisations, nous devons nous familiariser avec deux autres interfaces:

  • WriterTo;
  • Lire de.

Situation hypothetique. Votre Reader fonctionne avec un tampon mémoire. Il l'a déjà déplacé, écrit, lit quelque chose à partir de là, c'est-à-dire qu'un endroit sous lui a déjà été déplacé. Vous voulez lire ce Reader quelque part de l'extérieur.

Nous avons déjà vu comment cela se produit: un tampon est créé, le tampon est passé, qui est passé à la méthode Read; Le Reader, qui fonctionne avec la mémoire, le jette hors de la pièce répliquée ... Mais ce n'est plus optimal - l'endroit a été repositionné. Pourquoi recommencer?



Il y a 5-6 ans (il y a un lien vers la liste des modifications), deux interfaces ont été créées: WriteTo et ReadFrom, qui sont implémentées localement. Reader implémente WriteTo et Writer implémente ReadFrom. Il s'avère que Reader, ayant une tranche avec des données déjà répliquées, peut éviter un emplacement supplémentaire et accepter les méthodes Write To Writer et passer un tampon disponible à l'intérieur.

Voici comment fonctionne l'implémentation de bytes.Buffer et bufio. Et si vous regardez à nouveau le dendrogramme, vous verrez que ces deux interfaces ne sont pas très populaires. Ils sont juste implémentés pour les types qui fonctionnent avec le tampon interne - où la mémoire est déjà déplacée. Cela ne vous aidera pas à éviter l'éloquence à chaque fois, mais uniquement si vous travaillez déjà avec une pièce déplacée.

ReaderFrom fonctionne de manière similaire (il n'est implémenté que par Writer). ReaderFrom lit l'intégralité de Reader, qui vient comme argument (avant EOF) et écrit quelque part dans l'implémentation interne de Writer.

Implémentation de CopyBuffer


Cet extrait montre l'implémentation de l'assistant copyBuffer. Ce CopyBuffer non exportable est utilisé sous le capot de io.Copy, CopyN et CopyBuffer.

Et ici, il y a une petite nuance qui mérite d'être mentionnée. CopyN a récemment été optimisé - indépendant de cette logique. C'est exactement l'optimisation dont j'ai parlé plus tôt: avant de créer un tampon supplémentaire de 32 Ko, une vérification a lieu - peut-être que la source de données implémente l'interface WriterTo, et ce tampon supplémentaire n'est pas nécessaire?

Si cela ne se produit pas, nous vérifions: peut-être que Writer implémente ReaderFrom pour les connecter sans cet intermédiaire? Si cela ne se produit pas, le dernier espoir demeure: peut-être nous a-t-on donné une sorte de tampon déplacé que nous pourrions utiliser?



Voilà comment fonctionne io.Copy.

Il y a un problème, qui est une semi-proposition, un semi-bug - on ne sait pas quoi. Il est suspendu depuis un an et demi. Cela ressemble à ceci: CopyBuffer est sémantiquement incorrect.

Malheureusement, il n'y a pas de signature pour ce copyBuffer, mais il ressemble exactement à cette méthode non exportable.

Lorsque vous appelez copyBuffer dans l'espoir d'éviter un emplacement supplémentaire, transférez-y une sorte d'octet de tranche déplacé, la logique suivante fonctionne: si Reader ou Writer implémente les interfaces WriterTo et ReaderFrom, rien ne garantit que vous pourrez éviter cet emplacement. Cela a été accepté comme une proposition et a promis d'y penser dans Go 2.0. Pour l'instant, il vous suffit de savoir.

Travaillez avec io.Pipe. PipeReader et pipeWriter


Un autre cas: vous devez obtenir les données de Writer d'une manière ou d'une autre dans Reader. Joli étui de vie.

Imaginez que vous avez déjà des données, ils implémentent l'interface Reader - tout est clair avec cela. Vous devez compresser ces données, les «peaufiner» et les envoyer à S3. Quelle est la nuance? ..
Qui a travaillé avec le type gzip dans le package compess sait que le gzip'er lui-même n'est qu'un proxy: il prend les données en lui-même, implémente l'interface Writer, il écrit les données, quelque chose va leur faire, et alors je dois les déposer quelque part. Sur le constructeur, il faut une implémentation de l'interface Writer.

En conséquence, ici, nous avons besoin d'une sorte d'écrivain intermédiaire, où nous supprimerons les données déjà compressées qui sont archivées dans la première étape. Notre prochaine étape consiste à télécharger ces données sur S3. Et le client AWS standard accepte l'interface io.Reader comme source de données.



La diapositive montre le pipeline - il montre à quoi il ressemble: nous devons dépasser les données pour passer de Reader à Writer, de Writer à Reader. Comment faire?

La bibliothèque standard a une fonctionnalité intéressante - io.Pipe. Il renvoie deux valeurs: pipeReader et pipeWriter. Cette paire est inextricablement liée. Imaginez un «baby phone» dans des tasses avec des cordes: cela n'a aucun sens de parler dans une tasse alors que personne n'écoute à l'autre bout ...



Que fait cet io.Pipe? Il ne sera pas lu tant que personne n'aura écrit les données. Et vice versa, il n'écrira rien tant que personne n'aura lu ces données à l'autre bout. Voici un exemple d'implémentation:



nous ferons de même ici. Nous lirons le fichier robot.txt, qui a été lu auparavant, nous le compresserons à l'aide de notre gzip et l'enverrons à S3.

  • Sur la première ligne, une paire est créée - pipeReader, pipeWriter. Ensuite, nous devons exécuter au moins un goroutine, qui lira les données d'un bout (une sorte de pipe). Dans ce gorutin, exécutez l'uploader avec une source de données (source - pipeReader).
  • Dans l'étape suivante, nous devons compresser les données. Nous compressons les données et les écrivons dans pipeWriter (ce sera l'autre extrémité du tuyau), et déjà goroutine en cours d'exécution reçoit les données à l'autre extrémité du tuyau et les lit. Lorsque tout ce sandwich est prêt, il ne reste plus qu'à mettre le feu à la mèche ...
  • Voir: io.Copy sur la dernière ligne écrit les données du corps dans le gzip que nous avons créé (c'est-à-dire du lecteur à l'écrivain). Tout cela fonctionne comme prévu.

Cet exemple peut être résolu d'une autre manière. Si vous utilisez une implémentation qui implémente à la fois Reader et Writer. Vous allez d'abord y écrire des données, puis les lire.
C'était une démonstration claire de la façon de travailler avec io.Pipe.

Autres implémentations


C'est essentiellement tout pour moi. Nous arrivons à des implémentations intéressantes dont je voudrais parler.



Je n'ai rien dit sur MultiReader, ni sur MultiWriter. Et ceci est une autre implémentation sympa de la bibliothèque standard, qui vous permet de connecter différentes implémentations. Par exemple, MultiWriter écrit sur tous les écrivains simultanément et MultiReader lit les lecteurs de manière séquentielle.

Une autre implémentation est appelée limio. Il vous permet de définir une limite de soustraction. Vous pouvez définir la vitesse en octets par seconde à laquelle votre Reader doit être lu.

Une autre implémentation intéressante est juste une visualisation de la progression de la lecture - la barre de progression (d'un certain type). Cela s'appelle ioprogress.

Pourquoi ai-je dit tout ça? Qu'est-ce que je voulais dire par là?



  • Si vous devez soudainement implémenter les interfaces Reader et Writer, faites-le correctement. Il n'y a pas encore de décision unique qui est responsable de la mise en œuvre - nous supposerons que tout le monde fait confiance au contrat. Vous devez donc vous y conformer impeccablement.
  • Si votre cas fonctionne avec un tampon repositionné, n'oubliez pas les interfaces ReaderFrom et WriterTo.
  • Si vous êtes dans une impasse et que vous avez besoin d'exemples - regardez la bibliothèque standard, il existe de nombreuses implémentations intéressantes sur lesquelles vous pouvez compter. Il y a de la documentation là-bas.
  • Si quelque chose vous est complètement incompréhensible, n'hésitez pas à écrire des problèmes. Les gars là-bas sont adéquats, répondent rapidement, très poliment et vous aident avec compétence.



C’est tout pour moi. Merci d'être venu!

Des questions


Question du public (B): - J'ai une question simple, je suppose. Veuillez nous parler de quelques cas d'utilisation de la vie: lesquels ont été utilisés et pourquoi? Vous avez dit que Reader / Writer renvoie la longueur qu'il a lue. Avez-vous déjà rencontré des problèmes avec cela; quand avez-vous demandé à lire (pas seulement ReadAll existe), mais quelque chose n'a pas fonctionné?

SA: - Je dois honnêtement admettre que je n'ai jamais eu de tels cas, car j'ai toujours travaillé avec des implémentations de la bibliothèque standard. Mais hypothétiquement, une telle situation est bien sûr possible. En ce qui concerne les cas spécifiques, nous collectons souvent des tuyaux multicouches, et si vous autorisez hypothétiquement un tel bug, tout le tuyau s'effondrera ...

Q:- Ce n'est pas tout à fait un bug. Parlons alors de ma petite expérience. J'ai eu un problème avec Booking.com: ils ont utilisé le pilote que j'ai écrit, et ils ont eu un problème - quelque chose ne fonctionnait pas. Il y a un protocole binaire standard que nous avons fait; localement, tout fonctionne bien, tout le monde va bien, mais il s'est avéré qu'ils ont un très mauvais réseau avec un centre de données. Ensuite, Reader n'a pas vraiment tout renvoyé (cartes réseau défectueuses, autre chose).

CA: - Mais s'il n'a pas tout retourné, alors il n'aurait pas dû retourner le signe de la fin (end), et le client devrait revenir. En vertu du contrat qui est décrit, Reader ne devrait pas ... Disons simplement que Reader, bien sûr, décide quand il veut venir, quand il ne veut pas, cependant, s'il veut tout lire, il doit attendre EOF.

À:"Mais c'est précisément à cause de la connexion." C'est exactement le problème qui s'est produit dans le package net standard.

CA: - Et il a rendu l'EOF?

Q: - Il n'a pas tout rendu - il n'a tout simplement pas tout lu. Je lui ai dit: "Lisez les 20 octets suivants." Il lit. Et je ne lis pas tout.

SA: - Hypothétiquement, c'est possible, car ce n'est qu'une interface qui décrit un protocole de communication. Il est nécessaire de surveiller et de démonter spécifiquement le boîtier. Ici, je ne peux que vous répondre que le client, en théorie, aurait dû revenir s'il n'avait pas reçu tout ce qu'il voulait. Vous lui avez demandé une tranche de 20 octets, il a soustrait 15 pour vous, mais EOF n'est pas venu - vous devriez y retourner ...

Q: - Il y a io.ReadFull pour cette situation. Il est spécialement conçu pour lire la tranche jusqu'au bout.

CALIFORNIE:- Oui. Je n'ai rien dit sur ReadFull.

Q: - Il s'agit d'une situation tout à fait normale lorsque Read ne remplit pas la tranche entière. Vous devez vous y préparer.

SA: - C'est un cas très attendu!

Q: - Merci pour le rapport - c'était intéressant. J'utilise les lecteurs dans un petit proxy simple qui lit http et écrit dans l'autre sens. J'utilise Close Reader pour résoudre un problème - pour fermer ce que je lis tout le temps. Dois-je faire aveuglément confiance à un contrat? Vous avez dit qu'il pourrait y avoir des problèmes. Ou ajouter des chèques supplémentaires? Il est théoriquement possible que quelque chose ne vienne pas complètement sur ce site. Dois-je faire ces vérifications supplémentaires et ne pas faire confiance au contrat?

CALIFORNIE:- Je dirais ceci: si votre application tolère ces erreurs (par exemple, si vous faites entièrement confiance au contrat), alors peut-être pas. Mais si vous ne souhaitez pas avoir une «panique» en vous (comme je l'ai montré sur une lecture négative en octet.Buffer), je vérifierais quand même.
Mais cela dépend de vous. Que puis-je vous recommander? Je pense qu'il suffit de peser le pour et le contre. Que se passe-t-il si vous obtenez soudainement un nombre négatif d'octets?

Q: - Merci pour le rapport. Malheureusement, je ne sais rien de Go. Si une «panique» s'est produite, existe-t-il un moyen d'intercepter ces informations et d'obtenir des informations sur quoi, où, comment être biaisé, afin d'éviter les problèmes vendredi soir?

CA: - Oui. Le mécanisme de récupération vous permet de "rattraper" une panique et de la faire sortir sans tomber, relativement parlant.



À:- Comment vos recommandations d'utilisation des implémentations de Writer et Reader sont-elles cohérentes avec les erreurs renvoyées lors de l'implémentation de sockets Web. Je ne donnerai pas d'exemple concret, mais la fin de fichier y est-elle toujours utilisée? Pour autant que je m'en souvienne, le message se termine par d'autres significations ...

SA: - C'est une bonne question, car je n'ai rien à répondre. A regarder! Si l'EOF ne vient pas, alors le client, s'il veut tout récupérer, doit repartir.

Q: - Combien de temps le tuyau a-t-il pu être assemblé? Y a-t-il des croyances internes selon lesquelles la pipe ne vaut pas la peine de collecter plus de cinq participants, ou avec des branches? Combien de temps avez-vous réussi à construire un arbre à partir de ces tuyaux (lecture, écriture)?

CALIFORNIE:- Dans ma pratique, environ cinq appels consécutifs sont optimaux, car il est plus difficile de déboguer et de garder à l’esprit ce qui coule et où il va. On obtient une structure assez ramifiée. Mais je dirais quelque part 5-7 maximum.

Q: - 5-7 - dans quel cas?

SA: - C'est la lecture, par exemple, de certaines données. Vous devez vous engager, et ce que vous vous connectez, vous devez couper. Promis - alors vous lisez ces données - vous devez les renvoyer à un certain stockage (enfin, hypothétiquement). Dans tout stockage implémenté par l'interface Writer. Avec un tel tuyau, 5-6 étapes se produisent, bien qu'à l'une des étapes, il se ramifie toujours sur le côté et vous continuez à travailler avec Reader.

À:- Selon la méthode Débutant, vous avez eu une diapositive intéressante. Pouvez-vous indiquer 2-3 autres points intéressants qui étaient là, mais maintenant il vaut mieux ne pas les faire, mais le faire différemment maintenant?

SA: - Avec cette diapositive, je voulais montrer exactement comment le faire sans avoir besoin de lire Reader. Cela ne m'est jamais venu à l'esprit que quelque chose comme la méthode pour débutants ... C'est probablement la principale erreur, le motif principal à éviter lorsque l'on travaille avec des lecteurs.
Présentateur: - J'ajouterais moi-même qu'il est très important pour un débutant de lire toute la documentation du paquet io, sur toutes les interfaces qui sont là, et de les comprendre. Parce qu'en fait, il y en a beaucoup, et vous commencez souvent à faire quelque chose de vous-même, bien qu'il existe déjà là-bas et soit correctement mis en œuvre («à droite» - en tenant compte de toutes les fonctionnalités).
Question du leader: - Comment vivre plus loin?

CA: - Bonne question! J'ai promis de dire si nous avons le temps. À la suite de la discussion sur le bogue, LimitedReader a pris la décision suivante: fabriquer un préservatif Reader dans un certain sens, qui protège contre les menaces externes, envelopper un lecteur auquel vous ne faites pas confiance - pour empêcher toute infection d'entrer dans votre système.

Et dans ce Reader, vous implémentez toutes les vérifications que vous ne pouvez pas faire: par exemple, une lecture négative, des expériences avec le nombre d'octets (disons que vous avez envoyé une tranche de 10 octets, et vous en avez récupéré 15 - comment réagir à cela?) ... Dans ce Reader et vous pouvez implémenter un ensemble de ces contrôles. J'ai dit: "Peut-être ajoutons-nous à la bibliothèque standard, car il serait utile à tout le monde de l'utiliser"?

On m'a répondu que cela ne semble pas avoir de sens - c'est une chose simple que vous pouvez mettre en œuvre vous-même. Tout. Nous vivons. Nous faisons confiance aux gars du contrat. Mais je ne ferais pas confiance.



Q: - Lorsque nous travaillons avec des lecteurs, des écrivains et qu'il est possible de tomber sur une «bombe» gzip ... À quel point faisons-nous confiance à ReadAll et WriteAll? Ou, néanmoins, implémenter la lecture du tampon et travailler uniquement avec le tampon?

CALIFORNIE:- ReadAll lui-même n'utilise que des octets.Buffer sous le capot. Lorsque vous souhaitez utiliser telle ou telle chose, il est conseillé pour vous d'entrer et de voir comment ces "tripes" sont implémentées. Encore une fois, cela dépend de vos besoins: si vous ne tolérez pas les erreurs que j'ai montrées, vous devez voir si ce qui provient du Reader encapsulé est vérifié. S'il n'est pas coché, utilisez par exemple bufio (là tout est coché). Ou faites ce que je viens de dire: un certain lecteur proxy, qui, selon votre liste d'exigences, vérifiera ces données et les renverra au client ou les renverra au client.




Un peu de publicité :)


Merci de rester avec nous. Aimez-vous nos articles? Vous voulez voir des matériaux plus intéressants? Soutenez-nous en passant une commande ou en recommandant à vos amis des VPS basés sur le cloud pour les développeurs à partir de 4,99 $ , un analogue unique de serveurs d'entrée de gamme que nous avons inventés pour vous: Toute la vérité sur les VPS (KVM) E5-2697 v3 (6 cœurs) 10 Go DDR4 480 Go SSD 1 Gbit / s à partir de 19 $ ou comment diviser le serveur? (les options sont disponibles avec RAID1 et RAID10, jusqu'à 24 cœurs et jusqu'à 40 Go de DDR4).

Dell R730xd 2 fois moins cher au centre de données Equinix Tier IV à Amsterdam? Nous avons seulement 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV à partir de 199 $ aux Pays-Bas!Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - à partir de 99 $! En savoir plus sur la création d'un bâtiment d'infrastructure. classe c utilisant des serveurs Dell R730xd E5-2650 v4 coûtant 9 000 euros pour un sou?

All Articles