io.Reader & io.Writer
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 73 von 107.
Die Standardbibliothek von Go zeigt schnittstellenbasiertes Design anhand zweier grundlegender Schnittstellen: io.Reader und io.Writer. Diese einfachen Schnittstellen ermöglichen alles – von Dateioperationen bis hin zur Netzwerkkommunikation.
Das io.Reader-Interface erfordert nur eine Methode:
type Reader interface {
Read(p []byte) (n int, err error)
}Es liest bis zu len(p) bytes in das Slice ein und gibt zurück, wie viele bytes gelesen wurden. Ebenso definiert io.Writer:
type Writer interface {
Write(p []byte) (n int, err error)
}Jeder Typ, der diese Methoden implementiert, funktioniert mit dem gesamten I/O-Ökosystem.
Hier ist ein benutzerdefinierter Typ, der io.Reader implementiert:
type RepeatReader struct {
Char byte
Count int
pos int
}
func (r *RepeatReader) Read(p []byte) (int, error) {
if r.pos >= r.Count {
return 0, io.EOF
}
n := 0
for n < len(p) && r.pos < r.Count {
p[n] = r.Char
n++
r.pos++
}
return n, nil
}Jetzt funktioniert dieser benutzerdefinierte Reader mit jeder Funktion, die einen io.Reader erwartet:
reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data)) // AAAAAMit diesem Entwurf kannst du Funktionen schreiben, die io.Reader oder io.Writer akzeptieren, sodass sie mit Dateien, Netzwerkverbindungen, Puffern oder jeder benutzerdefinierten Implementierung funktionieren.
Aufgabe
EinfachLass uns einen benutzerdefinierten Textstream-Prozessor erstellen, der die Schnittstellen io.Reader und io.Writer implementiert! Du erstellst Typen, die Text während seines Durchlaufs transformieren können, und demonstrierst damit, wie diese grundlegenden Schnittstellen eine leistungsfähige I/O-Komposition ermöglichen.
Du organisierst deinen Code über zwei Dateien:
streams.go: Definiere deine benutzerdefinierten Reader- und Writer-Typen.Erstelle eine
CountingReader-Struktur, die einen String umschließt und verfolgt, wie viele Bytes gelesen wurden. Sie sollte Felder für den Quell-String und einen Positionszähler besitzen. Implementiere die MethodeRead(p []byte) (int, error), die Bytes aus der Quelle in das bereitgestellte Slice liest, die Position aktualisiert undio.EOFzurückgibt, wenn die Quelle erschöpft ist.Erstelle eine
UppercaseWriter-Struktur, die geschriebene Daten sammelt und in Großbuchstaben umwandelt. Sie sollte das angesammelte Ergebnis intern speichern. Implementiere die MethodeWrite(p []byte) (int, error), die eingehende Bytes in Großbuchstaben umwandelt und speichert. Füge eine MethodeResult() stringhinzu, um den angesammelten Text in Großbuchstaben abzurufen.Erstelle Konstruktorfunktionen
NewCountingReader(source string) *CountingReaderundNewUppercaseWriter() *UppercaseWriter, um deine Typen ordnungsgemäß zu initialisieren.main.go: Demonstriere, wie deine benutzerdefinierten I/O-Typen mit der Standardbibliothek funktionieren.Lies eine String-Eingabe ein und verwende dann deinen
CountingReadermitio.ReadAll, um alle Daten zu lesen. Übertrage diese Daten mithilfe derUppercaseWriter-Methode an deinenWrite.Gib die Ergebnisse in diesem Format aus:
Original: [input] Uppercase: [result from writer] Bytes read: [total bytes]
Die folgende Eingabe wird bereitgestellt:
- Eine einzelne Textzeile
Zum Beispiel bei folgender Eingabe:
Hello WorldDeine Ausgabe sollte wie folgt aussehen:
Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11Und bei folgender Eingabe:
Go interfaces are powerfulsollte deine Ausgabe wie folgt aussehen:
Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26Die zentrale Erkenntnis ist, dass dein CountingReader nahtlos mit io.ReadAll funktioniert, weil er io.Reader implementiert. Jede Funktion im Go-Ökosystem, die ein io.Reader akzeptiert, funktioniert mit deinem benutzerdefinierten Typ: Das ist die Stärke eines schnittstellenbasierten Designs.
Probier es selbst
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// Eingabe lesen
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
// Nachgestellten Zeilenumbruch entfernen, falls vorhanden
if len(input) > 0 && input[len(input)-1] == '\n' {
input = input[:len(input)-1]
}
// TODO: Erstelle einen CountingReader mit dem Eingabestring
// countingReader := NewCountingReader(input)
// TODO: Verwende io.ReadAll, um alle Daten von deinem CountingReader zu lesen
// data, err := io.ReadAll(countingReader)
// TODO: Erstelle einen UppercaseWriter
// upperWriter := NewUppercaseWriter()
// TODO: Schreibe die Daten in deinen UppercaseWriter
// upperWriter.Write(data)
// TODO: Gib die Ergebnisse im erforderlichen Format aus:
// Original: [input]
// Uppercase: [Ergebnis vom Writer]
// Bytes read: [Gesamtbytes]
// Platzhalter, um einen Fehler wegen ungenutztem Import zu vermeiden – beim Implementieren entfernen
_ = io.EOF
fmt.Println("Original:", input)
// fmt.Println("Uppercase:", upperWriter.Result())
// fmt.Println("Bytes read:", countingReader.Position)
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser11Standardbibliothek & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer-Interfaceencoding/json mit Structshttp.Handler-InterfaceRückblick – REST-API-Modelle3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler