Stas Afanasyev. Juno. Pipelines basierend auf io.Reader / io.Writer. Teil 1

In dem Bericht werden wir über das Konzept von io.Reader / io.Writer sprechen, warum sie benötigt werden, wie sie korrekt implementiert werden und welche Fallstricke diesbezüglich bestehen, sowie über das Erstellen von Pipelines basierend auf Standard- und benutzerdefinierten io.Reader / io.Writer-Implementierungen .



Stanislav Afanasyev (im Folgenden - SA): - Guten Tag! Ich heiße Stas. Ich kam aus Minsk, von der Firma Juno. Vielen Dank, dass Sie an diesem regnerischen Tag gekommen sind und die Kraft gefunden haben, das Haus zu verlassen.

Heute möchte ich mit Ihnen über ein Thema wie das Erstellen von Pipelines Go sprechen, das auf io.Reader / io.Writer-Schnittstellen basiert. Worüber ich heute sprechen werde, ist im Allgemeinen das Konzept der io.Reader / io.Writer-Schnittstellen, warum sie benötigt werden, wie man sie richtig verwendet und, was am wichtigsten ist, wie man sie richtig implementiert.

Wir werden auch über den Bau von Pipelines sprechen, die auf verschiedenen Implementierungen dieser Schnittstellen basieren. Wir werden über bestehende Methoden sprechen, deren Vor- und Nachteile diskutieren. Ich werde verschiedene Fallstricke erwähnen (dies wird im Überfluss vorhanden sein).

Bevor wir beginnen, müssen wir die Frage beantworten, warum diese Schnittstellen überhaupt benötigt werden. Heben Sie Ihre Hände, die mit Go eng zusammenarbeiten (jeden Tag, jeden zweiten Tag) ...



Großartig! Wir haben immer noch eine Go-Community. Ich denke, viele von Ihnen haben mit diesen Schnittstellen gearbeitet, zumindest davon gehört. Sie wissen vielleicht nicht einmal davon, aber Sie hätten sicherlich etwas über sie hören sollen.

Erstens sind diese Schnittstellen eine Abstraktion der Eingabe-Ausgabe-Operation in all ihren Erscheinungsformen. Zweitens ist es eine sehr praktische API, mit der Sie Pipelines wie einen Konstruktor aus Cubes erstellen können, ohne wirklich über die internen Details der Implementierung nachdenken zu müssen. Zumindest war das ursprünglich beabsichtigt.

io.Reader


Dies ist eine sehr einfache Schnittstelle. Es besteht nur aus einer Methode - der Read-Methode. Konzeptionell kann die Implementierung der io.Reader-Schnittstelle eine Netzwerkverbindung sein - beispielsweise wenn noch keine Daten vorhanden sind, diese jedoch dort angezeigt werden können:



Es kann sich um einen Puffer im Speicher handeln, in dem die Daten bereits vorhanden sind und vollständig ausgelesen werden können. Es kann auch ein Dateideskriptor sein - wir können diese Datei in Teilen lesen, wenn sie sehr groß ist.

Die konzeptionelle Implementierung der io.Reader-Schnittstelle ist der Zugriff auf einige Daten. Alle Fälle, die ich geschrieben habe, werden von der Read-Methode unterstützt. Es gibt nur ein Argument - dies ist Slice-Byte.
Ein Punkt, den Sie hier ansprechen sollten. Diejenigen, die kürzlich zu Go gekommen sind oder von einer anderen Technologie stammen, bei der es keine ähnliche API gab (ich bin eine davon), diese Signatur ist etwas verwirrend. Die Read-Methode scheint dieses Slice irgendwie zu lesen. In der Tat ist das Gegenteil der Fall: Die Reader-Schnittstellenimplementierung liest die darin enthaltenen Daten und füllt diesen Slice mit den Daten, die diese Implementierung enthält.

Die maximale Datenmenge, die auf Anfrage von der Read-Methode gelesen werden kann, entspricht der Länge dieses Slice. Eine reguläre Implementierung gibt so viele Daten zurück, wie zum Zeitpunkt der Anforderung zurückgegeben werden können, oder die maximale Menge, die in dieses Slice passt. Dies deutet darauf hin, dass Reader in Teilen gelesen werden kann: mindestens byteweise, mindestens zehn - wie Sie möchten. Und der Client, der Reader gemäß den Rückgabewerten der Read-Methode aufruft, überlegt, wie er weiterleben soll.

Die Read-Methode gibt zwei Werte zurück:

  • Anzahl der abgezogenen Bytes;
  • ein Fehler, wenn es aufgetreten ist.

Diese Werte beeinflussen das weitere Verhalten des Kunden. Auf der Folie befindet sich ein GIF, das diesen soeben beschriebenen Vorgang anzeigt:





Io.Reader - Wie geht das?


Es gibt genau zwei Möglichkeiten, wie Ihre Daten die Reader-Oberfläche erfüllen.



Der erste ist der einfachste. Wenn Sie eine Art Slice-Byte haben und möchten, dass es die Reader-Schnittstelle erfüllt, können Sie die Implementierung einer Standardbibliothek übernehmen, die diese Schnittstelle bereits erfüllt. Zum Beispiel Reader aus dem Byte-Paket. Auf der Folie oben sehen Sie die Signatur, wie dieser Reader erstellt wurde.

Es gibt einen komplizierteren Weg - die Reader-Oberfläche selbst zu implementieren. Die Dokumentation enthält ungefähr 30 Zeilen mit kniffligen Regeln, Einschränkungen, die befolgt werden müssen. Bevor wir über alle sprechen, wurde es für mich interessant: „Und in welchen Fällen reichen nicht genügend Standardimplementierungen (Standardbibliothek) aus? Wann müssen wir die Reader-Schnittstelle selbst implementieren? “

Um diese Frage zu beantworten, nahm ich tausend der beliebtesten Repositories auf Github (nach Anzahl der Sterne), fügte sie hinzu und fand dort alle Implementierungen der Reader-Oberfläche. Auf der Folie habe ich einige Statistiken (kategorisiert) darüber, wann Leute diese Schnittstelle implementieren.

  • Die beliebteste Kategorie sind Verbindungen. Dies ist eine Implementierung von proprietären Protokollen und Wrappern für vorhandene Typen. Brad Fitzpatrick hat also ein Camlistore-Projekt - es gibt ein Beispiel in Form von statTrackingConn, das im Allgemeinen ein gewöhnlicher Wrapper über den Con-Typ aus dem Netzpaket ist (fügt diesem Typ Metriken hinzu).
  • Die zweitbeliebteste Kategorie sind benutzerdefinierte Puffer. Hier hat mir das einzige Beispiel gefallen: dataBuffer aus dem x / net-Paket. Seine Besonderheit ist, dass es in Chunks geschnittene Daten speichert und beim Subtrahieren diese Chunks durchläuft. Wenn die Daten im Block vorbei sind, werden sie zum nächsten Block weitergeleitet. Gleichzeitig berücksichtigt er die Länge, die Stelle, an der er die übertragene Scheibe ausfüllen kann.
  • Eine andere Kategorie sind alle Arten von Fortschrittsbalken, die die Anzahl der Bytes zählen, die beim Senden von Metriken abgezogen werden ...

Basierend auf diesen Daten können wir sagen, dass die Notwendigkeit, die io.Reader-Schnittstelle zu implementieren, ziemlich häufig auftritt. Lassen Sie uns dann über die Regeln sprechen, die in der Dokumentation enthalten sind.

Dokumentationsregeln


Wie gesagt, die Liste der Regeln und im Allgemeinen die Dokumentation ist ziemlich umfangreich und umfangreich. 30 Zeilen reichen für eine Schnittstelle, die nur aus drei Zeilen besteht.

Die erste, wichtigste Regel betrifft die Anzahl der zurückgegebenen Bytes. Sie muss streng größer oder gleich Null und kleiner oder gleich der Länge des gesendeten Slice sein. Warum ist es wichtig?



Da dies ein ziemlich strenger Vertrag ist, kann der Kunde dem Betrag vertrauen, der aus der Implementierung stammt. In der Standardbibliothek befinden sich Wrapper (z. B. bytes.Buffer und bufio). Es gibt einen solchen Moment in der Standardbibliothek: Einige Implementierungen vertrauen verpackten Lesern, andere vertrauen nicht (wir werden später darüber sprechen).

Bufio vertraut überhaupt nichts - es überprüft absolut alles. Bytes.Buffer vertraut absolut allem, was zu ihm kommt. Jetzt werde ich zeigen, was im Zusammenhang damit passiert ...

Wir werden nun drei mögliche Fälle betrachten - dies sind drei implementierte Leser. Sie sind ziemlich synthetisch und nützlich für das Verständnis. Wir werden alle diese Leser mit dem ReadAll-Helfer lesen. Seine Unterschrift befindet sich oben auf der Folie:



io.Reader # 1. Beispiel 1


ReadAll ist ein Helfer, der eine Art Implementierung der Reader-Schnittstelle übernimmt, alles liest und die gelesenen Daten sowie einen Fehler zurückgibt.

Unser erstes Beispiel ist Reader, der immer -1 und nil als Fehler zurückgibt, d. H. Einen solchen NegativeReader. Lassen Sie es uns laufen und sehen, was passiert:



Wie Sie wissen, ist Panik ohne Grund ein Zeichen von Dummheit. Aber wer in diesem Fall Dummkopf ist - ich oder Byte. Puffer - hängt vom Standpunkt ab. Diejenigen, die dieses Paket schreiben und es befolgen, haben unterschiedliche Sichtweisen.

Was ist hier passiert? Bytes.Buffer akzeptierte eine negative Anzahl von Bytes, überprüfte nicht, ob es negativ war, und versuchte, den internen Puffer entlang der oberen Grenze, den er erhielt, abzuschneiden - und wir kamen aus den Slice-Grenzen heraus.

In diesem Beispiel gibt es zwei Probleme. Das erste ist, dass die Signatur nicht verboten ist, negative Zahlen zurückzugeben, und die Dokumentation ist verboten. Wenn die Signatur Uint hätte, würden wir einen klassischen Überlauf bekommen (wenn eine signierte Nummer als nicht signiert interpretiert wird). Und dies ist ein sehr kniffliger Fehler, der sicherlich am Freitagabend auftreten wird, wenn Sie bereits zu Hause versammelt sind. Daher ist Panik in diesem Fall die bevorzugte Option.

Der zweite „Punkt“ ist, dass der Stack-Trace überhaupt nicht versteht, was passiert ist. Es ist klar, dass wir die Grenzen der Scheibe überschritten haben - na und? Wenn Sie eine solche mehrschichtige Pipe haben und ein solcher Fehler auftritt, ist nicht sofort klar, was passiert ist. Das Bufio der Standardbibliothek gerät in dieser Situation ebenfalls in Panik, macht es aber schöner. Er schreibt sofort: „Ich habe eine negative Anzahl von Bytes abgezogen. Ich werde nichts anderes tun - ich weiß nicht, was ich damit anfangen soll. "

Und Bytes. Puffer gerät in Panik, so gut er kann. Ich habe ein Problem in Golang gepostet und mich gebeten, einen menschlichen Fehler hinzuzufügen. Am dritten Tag diskutierten wir die Aussichten dieser Entscheidung. Der Grund ist folgender: Historisch gesehen haben verschiedene Menschen zu unterschiedlichen Zeiten unterschiedliche unkoordinierte Entscheidungen getroffen. Und jetzt haben wir Folgendes: In einem Fall vertrauen wir der Implementierung überhaupt nicht (wir überprüfen alles), und im anderen Fall vertrauen wir voll und ganz, wir bekommen nicht, was von dort kommt. Dies ist ein ungelöstes Problem, und wir werden mehr darüber sprechen.

io.Reader # 1. Beispiel 2


Die folgende Situation: Unser Reader gibt immer 0 und Null als Ergebnis zurück. Aus vertraglicher Sicht ist hier alles legal - es gibt keine Probleme. Die einzige Einschränkung: In der Dokumentation heißt es, dass Implementierungen nicht empfohlen werden, zusätzlich zu dem Fall, in dem die Länge des gesendeten Slice Null ist, die Werte 0 und Null zurückzugeben.

Im wirklichen Leben kann ein solcher Leser viel Ärger verursachen. Wir kehren also zu der Frage zurück, ob wir Reader vertrauen sollen. Zum Beispiel ist eine Prüfung in bufio integriert: Sie liest Reader genau 100 Mal nacheinander. Wenn ein solches Wertepaar 100 Mal zurückgegeben wird, gibt es einfach NoProgress zurück.

Es gibt nichts Vergleichbares in Bytes. Puffer. Wenn wir dieses Beispiel ausführen, erhalten wir nur eine Endlosschleife (ReadAll verwendet Bytes. Puffer unter der Haube, nicht Reader selbst):



io.Reader # 1. Beispiel 2


Noch ein Beispiel. Es ist auch ziemlich synthetisch, aber nützlich für das Verständnis:



Hier geben wir immer 1 und Null zurück. Auch hier scheint es keine Probleme zu geben - aus vertraglicher Sicht ist alles legal. Es gibt eine Nuance: Wenn ich dieses Beispiel auf meinem Computer ausführe, friert es nach 30 Sekunden ein ...

Dies liegt daran, dass der Client, der diesen Reader liest (dh bytes.Buffer), niemals ein Zeichen für das Ende der Daten erhält - er liest, subtrahiert ... Außerdem erhält er jedes Mal ein subtrahiertes Byte. Für ihn bedeutet dies, dass der neu positionierte Puffer irgendwann endet, immer noch läuft - die Situation wiederholt sich und er läuft bis ins Unendliche, bis er platzt.

io.Reader # 2. Fehlerrückgabe


Wir kommen zu der zweiten wichtigen Regel für die Implementierung der Reader-Schnittstelle - dies ist eine Fehlerrückgabe. Die Dokumentation enthält drei Fehler, die die Implementierung zurückgeben sollte. Der wichtigste von ihnen ist EOF.

EOF ist das eigentliche Zeichen für das Ende der Daten, das die Implementierung zurückgeben sollte, wenn die Daten ausgehen. Konzeptionell ist dies im Allgemeinen kein Fehler, sondern ein Fehler.

Es gibt einen weiteren Fehler namens UnexpectedEOF. Wenn der Reader beim Lesen plötzlich die Daten nicht mehr lesen kann, wurde angenommen, dass er UnexpectedEOF zurückgeben würde. Tatsächlich wird dieser Fehler jedoch nur an einer Stelle der Standardbibliothek verwendet - in der ReadAtLeast-Funktion.



Ein weiterer Fehler ist NoProgress, über den wir bereits gesprochen haben. Die Dokumentation sagt es so: Dies ist ein Zeichen dafür, dass die Schnittstelle implementiert ist, ist zum Kotzen.

Io.Reader # 3


Die Dokumentation enthält eine Reihe von Fällen, wie der Fehler korrekt zurückgegeben werden kann. Unten sehen Sie drei mögliche Fälle:



Wir können einen Fehler sowohl mit abgezogener Anzahl von Bytes als auch separat zurückgeben. Wenn Ihre Daten jedoch plötzlich in Ihrem Reader aufgebraucht sind und Sie das EOF [Endzeichen] momentan nicht zurückgeben können (viele Implementierungen der Standardbibliothek funktionieren einfach so), wird davon ausgegangen, dass Sie EOF zum nächsten aufeinander folgenden Aufruf zurückgeben (dh Sie müssen loslassen Kunde).

Für den Kunden bedeutet dies, dass es keine Daten mehr gibt - kommen Sie nicht mehr zu mir. Wenn Sie null zurückgeben und der Kunde Daten benötigt, sollte er erneut zu Ihnen kommen.

io.Reader. Fehler


Im Allgemeinen waren dies laut Reader die wichtigsten Regeln. Es gibt immer noch eine Reihe kleinerer, aber sie sind nicht so wichtig und führen nicht zu einer solchen Situation:



Bevor wir alles im Zusammenhang mit Reader durchgehen, müssen wir die Frage beantworten: Ist es wichtig, dass bei benutzerdefinierten Implementierungen häufig Fehler auftreten? Um diese Frage zu beantworten, habe ich mich an meine Spool für 1000 Repositorys gewandt (und dort haben wir ungefähr 550 benutzerdefinierte Implementierungen). Ich schaute mit meinen Augen durch die ersten hundert. Natürlich ist dies keine Superanalyse, aber was es ist ... Ich habe

die beiden beliebtesten Fehler identifiziert:
  • gibt niemals EOF zurück;
  • zu viel Vertrauen in den verpackten Reader.

Auch dies ist aus meiner Sicht ein Problem. Und für diejenigen, die sich das io-Paket ansehen, ist dies kein Problem. Wir werden noch einmal darüber sprechen.

Ich möchte auf eine Nuance zurückkommen. Siehe:



Der Client sollte das Paar 0 und Null niemals als EOF interpretieren. Das ist ein Fehler! Für Reader ist dieser Wert nur eine Gelegenheit, den Client loszulassen. Die beiden Fehler, die ich erwähnt habe, scheinen unbedeutend zu sein, aber es reicht aus, sich vorzustellen, dass Sie eine mehrschichtige Pipeline in der Prod und einen kleinen, schlauen "Bagul" in der Mitte haben, dann wird das "unterirdische Klopfen" nicht lange dauern - garantiert!

Laut Reader im Grunde alles. Dies waren die grundlegenden Implementierungsregeln.

io.Writer


Am anderen Ende der Pipelines haben wir io.Writer, wo wir normalerweise Daten schreiben. Eine sehr ähnliche Schnittstelle: Sie besteht ebenfalls aus einer Methode (Write), deren Signatur ähnlich ist. Aus semantischer Sicht ist die Writer-Oberfläche verständlicher: Ich würde sagen, dass sie, wie sie gehört wird, geschrieben ist.



Die Write-Methode nimmt ein Slice-Byte und schreibt es vollständig. Er hat auch eine Reihe von Regeln, die befolgt werden müssen.

  1. Die erste betrifft die zurückgegebene Anzahl der geschriebenen Bytes. Ich würde sagen, dass es nicht so streng ist, weil ich kein einziges Beispiel gefunden habe, das zu einigen kritischen Konsequenzen führen würde - zum Beispiel zu Panik. Dies ist nicht sehr streng, da es die folgende Regel gibt ...
  2. Die Writer-Implementierung muss einen Fehler zurückgeben, wenn die geschriebene Datenmenge geringer ist als die gesendete. Das heißt, eine teilweise Aufzeichnung wird nicht unterstützt. Dies bedeutet, dass es nicht sehr wichtig ist, wie viele Bytes geschrieben wurden.
  3. Noch eine Regel: Writer sollte das gesendete Slice auf keinen Fall ändern, da der Client weiterhin mit diesem Slice arbeitet.
  4. Der Writer sollte dieses Slice nicht halten (Reader hat die gleiche Regel). Wenn Sie für einige Vorgänge Daten in Ihrer Implementierung benötigen, müssen Sie nur diese Folie kopieren.



Von Reader und Writer, das war's.

Dendrogramm


Speziell für diesen Bericht habe ich ein Implementierungsdiagramm erstellt und es in Form eines Dendrogramms entworfen. Diejenigen, die jetzt wollen, können diesem QR-Code folgen:



Dieses Dendrogramm enthält alle Implementierungen aller Schnittstellen des io-Pakets. Dieses Dendrogramm wird benötigt, um einfach zu verstehen: Was und mit was können Sie in den Pipelines zusammenhalten, wo und was können Sie lesen, wo können Sie schreiben. Ich werde im Verlauf meines Berichts weiterhin darauf verweisen. Bitte beziehen Sie sich daher auf den QR-Code.

Pipelines


Wir haben darüber gesprochen, was Reader, io.Writer ist. Lassen Sie uns nun über die API sprechen, die in der Standardbibliothek zum Erstellen von Pipelines vorhanden ist. Beginnen wir mit den Grundlagen. Vielleicht wird es für niemanden interessant sein. Dies ist jedoch sehr wichtig.

Wir werden die Daten aus dem Standardeingabestream (von Stdin) lesen:



Stdin wird in Go durch eine globale Variable vom Typ Datei aus dem Betriebssystempaket dargestellt. Wenn Sie sich das Dendrogramm ansehen, werden Sie feststellen, dass der Dateityp auch die Reader- und Writer-Schnittstellen implementiert.

Im Moment interessieren wir uns für Reader. Wir werden Stdin mit demselben ReadAll-Helfer vorlesen, den wir bereits verwendet haben.

Eine Nuance in Bezug auf diesen Helfer ist erwähnenswert: ReadAll liest den Reader bis zum Ende, bestimmt jedoch das Ende durch EOF, durch das Zeichen des Endes, über das wir gesprochen haben.
Wir werden jetzt die Datenmenge begrenzen, die wir von Stdin lesen. Zu diesem Zweck gibt es eine Implementierung von LimitedReader in der Standardbibliothek:



Ich möchte, dass Sie darauf achten, wie LimitedReader die Anzahl der zu lesenden Bytes begrenzt. Man würde denken, dass diese Implementierung, dieser Wrapper, alles, was sich im Reader befindet, subtrahiert, was er umschließt, und dann so viel gibt, wie wir wollen. Aber alles funktioniert ein bisschen anders ...

LimitedReader schneidet das ihm gegebene Slice als Argument entlang der oberen Grenze ab. Und er gibt diese beschnittene Scheibe an Reader weiter, der sie einwickelt. Dies ist eine klare Demonstration, wie die Länge der gelesenen Daten in den Implementierungen der io.Reader-Schnittstelle reguliert wird.

Fehler beim Zurückgeben des Dateiende


Ein weiterer interessanter Punkt: Beachten Sie, wie diese Implementierung einen EOF-Fehler zurückgibt! Die zurückgegebenen benannten Werte werden hier verwendet und durch die Werte zugewiesen, die wir vom umschlossenen Reader erhalten.

Und wenn sich im umschlossenen Reader mehr Daten befinden, als wir benötigen, weisen wir die Werte des umschlossenen Readers zu - beispielsweise 10 Byte und Null -, da sich noch Daten im umschlossenen Reader befinden. Aber die Variable n, die abnimmt (in der vorletzten Zeile), sagt, dass wir den „Boden“ erreicht haben - das Ende dessen, was wir brauchen.

In der nächsten Iteration sollte der Client erneut kommen - unter der ersten Bedingung erhält er EOF. Dies ist der Fall, den ich erwähnt habe.

Wird sehr bald fortgesetzt ...


Ein bisschen Werbung :)


Vielen Dank für Ihren Aufenthalt bei uns. Gefällt dir unser Artikel? Möchten Sie weitere interessante Materialien sehen? Unterstützen Sie uns, indem Sie eine Bestellung aufgeben oder Ihren Freunden Cloud-basiertes VPS für Entwickler ab 4,99 US-Dollar empfehlen , ein einzigartiges Analogon von Einstiegsservern, das von uns für Sie erfunden wurde: Die ganze Wahrheit über VPS (KVM) E5-2697 v3 (6 Kerne) 10 GB DDR4 480 GB SSD 1 Gbit / s ab 19 $ oder wie teilt man den Server? (Optionen sind mit RAID1 und RAID10, bis zu 24 Kernen und bis zu 40 GB DDR4 verfügbar).

Dell R730xd 2-mal günstiger im Equinix Tier IV-Rechenzentrum in Amsterdam? Nur wir haben 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2,6 GHz 14C 64 GB DDR4 4 x 960 GB SSD 1 Gbit / s 100 TV von 199 US-Dollar in den Niederlanden!Dell R420 - 2x E5-2430 2,2 GHz 6C 128 GB DDR3 2x960 GB SSD 1 Gbit / s 100 TB - ab 99 US-Dollar! Lesen Sie mehr über den Aufbau eines Infrastrukturgebäudes. Klasse C mit Dell R730xd E5-2650 v4-Servern für 9.000 Euro für einen Cent?

All Articles