© SuS_Media
Ein- und Ausgabe über den Datenstrom

io.Reader und io.Writer in Go erklärt


Go-Anfänger sollten verstehen, wie das io-Paket der Sprache funktioniert. Welche Möglichkeiten bietet es und wie können diese im eigenen Code genutzt werden? Darauf geht dieser Artikel ein.

Nahezu jedes Programm verarbeitet während der Laufzeit Daten. Diese können dabei aus unterschiedlichsten Quellen stammen. Ein Server liest beispielsweise Einträge aus Datenbanken, ein Texteditor liest eine Textdatei vom Filesystem oder ein Browser lädt Webseiten per HTTP. Das Einlesen und Verarbeiten erfolgen dabei in der Regel sequenziell. Das bedeutet, dass Daten nacheinander eingelesen werden.

Dieser Artikel beschreibt, wie Datenströme in Go abgebildet werden und wie sie im Code verwendet werden können. Er baut auf die Grundlagen der Syntax auf und gibt einen kurzen Überblick über das io-Paket. Besonders Go-Anfänger sollten sich einmal mit diesem Paket beschäftigen, da dieses die wichtigsten Abstraktionen der Standardbibliothek beinhaltet. Denn über die Interfaces io.Reader und io.Writer lassen sich Daten einlesen und ausgeben. Idiomatischer Go-Code sollte diese Interfaces immer dann verwenden, wenn Daten verarbeitet werden. Das ist am Anfang ungewohnt, führt aber auf Dauer zu besseren Schnittstellen und flexiblerem Code. Die Verwendung von io.Reader und io.Writer zieht sich auch stringent durch die Standardbibliothek.

io.Reader: die Basics

Bevor wir uns näher mit den weiteren Features des io-Pakets befassen, wird hier zuerst die grundsätzliche Funktionsweise dieser Abstraktion betrachtet. Der io.Reader besitzt nur die Read-Methode. Sie hat als Input ein []byte und als Output die Werte n und err.

type Reader interface { Read(p []byte) (n int, err error) }

An dieser Stelle ist wichtig zu wissen, wie in Go ein Slice intern abgbildet wird. Dieses ist dabei ein Pointer auf ein Array. Die Variable p zeigt auf einen Bereich im Arbeitsspeicher, der innerhalb der Read-Methode beschrieben wird. Der Reader schreibt somit in das übergebene Slice hinein. Der Rückgabewert n beinhaltet dabei die Anzahl der Bytes, die geschrieben wurden, und err wird für das Melden von Fehlern verwendet.

Dieser Aufbau mag anfangs vielleicht ein wenig kompliziert anmuten, jedoch hat er einen entscheidenden Vorteil. Denn dadurch ist nicht der Reader dafür verantwortlich, Speicher für den Datenstrom bereitzustellen. Durch das Erzeugen von p definiert unser Code, wie groß die einzelnen Stücke des Datenstroms sind.

Sobald ein Datenstrom zu Ende ist, wird über err der Wert io.EOF (end of file) gemeldet. Gemäß Dokumentation dürfen danach keine weiteren Daten bei weiteren Read-Aufrufen geschrieben werden. Das bedeutet, dass ein Datenstrom aus einem io.Reader nur einmal ausgelesen werden kann.

Um zu verstehen, wie der io.Reader funktioniert, lesen wir einfach mal alle Daten aus einem Reader aus. Hierfür rufen wir die Read-Methode in einer for-Schleife auf. Diese Schleife wird beendet, sobald io.EOF als Fehler gemeldet wird. Die Daten werden in buf eingelesen. Da diese Variable ein Slice ist, zeigt es auf ein Array. Die Größe des Arrays wird durch die make-Funktion definiert. Sie legt im Hintergrund das Array an und reserviert damit entsprechenden Platz im Arbeitsspeicher. Zum Schluss wird die Anzahl der gelesenen Bytes und deren Inhalt als String ausgegeben. Mit buf[:n] wird die Ausgabe immer auf die gelesenen Bytes beschränkt (Listing 1).

Listing 1: Daten aus einem Reader über eine for-Schleife auslesen

func main() { r := strings.NewReader("Hallo ich lese hier Text aus") buf := make([]byte, 5) for { n, err := r.Read(buf) if err == io.EOF { break } fmt.Println(n, string(buf[:n])) } }

Da diese Abstraktion so tief in der Standardbibliothek verwurzelt ist, muss in der...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang