strings.Reader ist der kleinste mögliche Brückenkopf zwischen der String-Welt und der io-Welt. Wer einen String in der Hand hat und eine API bedienen muss, die einen io.Reader verlangt — json.Decoder, bufio.Scanner, http.NewRequest, csv.NewReader —, packt den String in einen strings.Reader und ist fertig. Es wird dabei nichts kopiert: der Reader hält nur einen Verweis auf den ursprünglichen String und einen Offset, der den aktuellen Lesefortschritt markiert.
Das Besondere ist die Breite der implementierten Interfaces. Ein strings.Reader ist nicht nur io.Reader, sondern gleichzeitig io.Seeker, io.ReaderAt, io.ByteReader/io.ByteScanner, io.RuneReader/io.RuneScanner und io.WriterTo. Damit deckt ein einziger Typ praktisch alle Lese-Modi ab, die die Standard-Bibliothek kennt. Die einzige Erzeugungsform ist der Konstruktor NewReader(s) — direkte Literale gibt es nicht.
Erzeugung mit NewReader
Ein strings.Reader wird ausschließlich über NewReader angelegt. Die Funktion ist trivial — sie speichert den String und initialisiert den internen Offset auf 0 — aber sie ist der offizielle Einstiegspunkt und der einzige, den man verwenden sollte. Das Zero-Value strings.Reader{} ist zwar technisch verwendbar, aber nicht Teil des dokumentierten APIs.
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hallo, Welt!")
buf := make([]byte, 5)
n, err := r.Read(buf)
fmt.Printf("gelesen=%d err=%v daten=%q\n", n, err, buf[:n])
// Verbleibende Bytes und Gesamtgröße
fmt.Printf("Len()=%d Size()=%d\n", r.Len(), r.Size())
// Rest lesen
rest, _ := io.ReadAll(r)
fmt.Printf("rest=%q\n", rest)
}gelesen=5 err=<nil> daten="Hallo"
Len()=7 Size()=12
rest=", Welt!"Size() liefert die ursprüngliche Stringlänge in Bytes und bleibt über die gesamte Lebensdauer des Readers konstant. Len() dagegen zeigt nur die noch nicht gelesenen Bytes — die Differenz ist die aktuelle Position. Beide Methoden sind O(1) und allokationsfrei.
Welche io-Interfaces deckt Reader ab
strings.Reader ist mit Absicht so breit aufgestellt: alles, was man mit einem read-only Byte-Strom sinnvoll machen kann, ist hier verfügbar. Das macht den Typ zum De-facto-Standard, wenn ein String an eine io-API gereicht werden soll.
| Interface | Methode(n) | Zweck |
|---|---|---|
io.Reader | Read(p []byte) | Standard-Lesepfad |
io.ReaderAt | ReadAt(p []byte, off int64) | Random-Access ohne State-Mutation |
io.ByteReader | ReadByte() | byte-weiser Zugriff |
io.ByteScanner | UnreadByte() | Lookahead um ein Byte |
io.RuneReader | ReadRune() | UTF-8-dekodierter Zugriff |
io.RuneScanner | UnreadRune() | Lookahead um eine Rune |
io.Seeker | Seek(off, whence) | Position ändern |
io.WriterTo | WriteTo(w Writer) | direkter Transfer in einen Writer |
Die WriteTo-Methode ist besonders interessant, wenn man den Reader an io.Copy übergibt: io.Copy erkennt das Interface und überspringt den Zwischenpuffer — der String wird direkt in den Ziel-Writer geschrieben.
Standard-Lesepfad mit Read
Read verhält sich exakt so, wie das io.Reader-Interface es vorschreibt: es kopiert bis zu len(p) Bytes aus dem zugrundeliegenden String in den Puffer und gibt die Anzahl der gelesenen Bytes plus einen möglichen Fehler zurück. Ist das Ende erreicht, liefert die nächste Read-Operation (0, io.EOF).
package main
import (
"errors"
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("ABCDEFGHIJ")
buf := make([]byte, 4)
for {
n, err := r.Read(buf)
if n > 0 {
fmt.Printf("chunk=%q verbleibend=%d\n", buf[:n], r.Len())
}
if errors.Is(err, io.EOF) {
fmt.Println("EOF erreicht")
break
}
}
}chunk="ABCD" verbleibend=6
chunk="EFGH" verbleibend=2
chunk="IJ" verbleibend=0
EOF erreichtBeachte den klassischen io.Reader-Kontrakt: ein Aufruf kann auch dann n > 0 zurückgeben, wenn gleichzeitig ein Fehler signalisiert wird. Bei strings.Reader ist das in der Praxis selten relevant, weil keine I/O passiert — aber generische io-Code-Pfade sollten den Vertrag trotzdem beachten.
Mehrfaches Lesen ohne neue Allokation
Da strings.Reader io.Seeker implementiert, kann man die Leseposition jederzeit ändern. Der typische Use-Case: einen String zweimal lesen, ohne einen neuen Reader anzulegen — etwa weil ein Decoder beim ersten Versuch fehlschlägt und man einen Fallback probieren möchte.
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Zeile-A\nZeile-B\n")
// erster Durchlauf
first, _ := io.ReadAll(r)
fmt.Printf("1. Pass: %q (Len=%d)\n", first, r.Len())
// zurück zum Anfang
_, _ = r.Seek(0, io.SeekStart)
fmt.Printf("nach Seek: Len=%d\n", r.Len())
// zweiter Durchlauf
second, _ := io.ReadAll(r)
fmt.Printf("2. Pass: %q\n", second)
}1. Pass: "Zeile-A\nZeile-B\n" (Len=16)
nach Seek: Len=16
2. Pass: "Zeile-A\nZeile-B\n"Die whence-Konstanten io.SeekStart, io.SeekCurrent und io.SeekEnd haben dieselbe Semantik wie bei Dateien. Ein negativer Offset oder ein Offset hinter dem String-Ende führt zu einem Fehler — der Reader bleibt dann an seiner alten Position.
Byte- und Rune-weiser Zugriff mit Lookahead
Parser, die Token-für-Token arbeiten, profitieren von ReadByte/ReadRune: sie sparen den Allokations-Overhead eines Slice-Puffers und liefern direkt das nächste Element. Die Unread*-Pendants schieben das zuletzt gelesene Element zurück — praktisch für Ein-Token-Lookahead in handgeschriebenen Parsern.
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("Go: Gänse, Größe, ß")
// byte-weise (ASCII-Präfix)
for i := 0; i < 3; i++ {
b, _ := r.ReadByte()
fmt.Printf("byte=%q ", b)
}
fmt.Println()
// Lookahead: letztes Byte zurückschieben
_ = r.UnreadByte()
// rune-weise (Multi-Byte UTF-8 korrekt)
for {
ru, size, err := r.ReadRune()
if err != nil {
break
}
fmt.Printf("rune=%q(%dB) ", ru, size)
}
}byte='G' byte='o' byte=':'
rune=':'(1B) rune=' '(1B) rune='G'(1B) rune='ä'(2B) rune='n'(1B) rune='s'(1B) rune='e'(1B) rune=','(1B) rune=' '(1B) rune='G'(1B) rune='r'(1B) rune='ö'(2B) rune='ß'(2B) rune='e'(1B) rune=','(1B) rune=' '(1B) rune='ß'(2B)ReadRune liest die nächste UTF-8-Sequenz vollständig und gibt zusätzlich die Byte-Länge zurück — wichtig, wenn man eigene Offsets mitführt. UnreadRune funktioniert nur direkt nach einem erfolgreichen ReadRune und schiebt genau diese Rune zurück.
Random-Access ohne State-Mutation
ReadAt(buf, off) liest aus einem absoluten Offset, ohne die interne Position des Readers zu verändern. Das macht den Aufruf threadsafe: mehrere Goroutines können gleichzeitig aus demselben *strings.Reader an unterschiedlichen Offsets lesen, ohne sich zu beeinflussen.
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("0123456789ABCDEF")
buf := make([]byte, 4)
n, _ := r.ReadAt(buf, 10)
fmt.Printf("ab Offset 10: %q (n=%d)\n", buf[:n], n)
n, _ = r.ReadAt(buf, 2)
fmt.Printf("ab Offset 2: %q (n=%d)\n", buf[:n], n)
// interne Position bleibt bei 0
fmt.Printf("Len() unverändert: %d\n", r.Len())
}ab Offset 10: "ABCD" (n=4)
ab Offset 2: "2345" (n=4)
Len() unverändert: 16Im Gegensatz zu Read liefert ReadAt einen Fehler (typischerweise io.EOF), sobald weniger Bytes verfügbar sind als angefordert — auch wenn n > 0 ist. Das ist Teil des io.ReaderAt-Kontrakts und unterscheidet die Methode bewusst vom lockereren io.Reader-Vertrag.
Pool-freundliches Recycling
Reset(s string) bindet einen vorhandenen *strings.Reader an einen neuen String. Die Methode allokiert nichts; sie überschreibt nur den internen String-Verweis und setzt den Offset zurück. Das ist wertvoll, wenn man Reader in einer sync.Pool hält und für jede Anfrage recycelt — etwa in einem Hot-Path-Parser.
package main
import (
"fmt"
"io"
"strings"
"sync"
)
var readerPool = sync.Pool{
New: func() any { return strings.NewReader("") },
}
func parse(s string) string {
r := readerPool.Get().(*strings.Reader)
defer readerPool.Put(r)
r.Reset(s) // gleichen Reader für neuen Input nutzen
out, _ := io.ReadAll(r)
return string(out)
}
func main() {
fmt.Println(parse("erste Anfrage"))
fmt.Println(parse("zweite Anfrage"))
fmt.Println(parse("dritte Anfrage"))
}erste Anfrage
zweite Anfrage
dritte AnfrageIm Pool-Pattern spart man pro Anfrage genau eine strings.Reader-Allokation (ca. 24 Byte). Bei einem HTTP-Handler mit hohem Durchsatz summiert sich das messbar — und der Code bleibt lesbar, weil Reset semantisch genau das ausdrückt, was man möchte.
Wann Reader, wann Buffer
Beide Typen erzeugen aus einem String einen io.Reader — aber sie unterscheiden sich grundlegend in Semantik und Kosten. Die Faustregel: read-only und seekable → strings.Reader; lesen-und-schreiben oder unbekannte Folge-Operationen → bytes.Buffer.
| Aspekt | strings.NewReader(s) | bytes.NewBufferString(s) |
|---|---|---|
| Mutation | read-only | read-write |
| String-Kopie | nein (nur Verweis) | ja (kopiert in []byte) |
| Allokation | ~24 Byte | ~24 Byte + len(s) |
io.Seeker | ja | nein |
io.ReaderAt | ja | nein |
| Reset | Reset(string) | Reset() (leert) |
| Threadsafe-Reads | ReadAt ja, Read nein | nein |
Wer einen String einmalig an einen Decoder reichen will, fährt mit strings.NewReader immer besser — es ist schlanker und kann mehr. bytes.Buffer lohnt sich erst, wenn man danach in denselben Puffer schreiben oder Bytes anhängen möchte.
Konfiguration aus einem String parsen
Ein klassisches Einsatzfeld: ein JSON-Schnipsel liegt als String vor — aus einem Environment-Variable, einem Test-Fixture oder einer Datenbank-Spalte — und soll dekodiert werden. json.NewDecoder will einen io.Reader, und genau dafür ist strings.NewReader gemacht. Die Alternative json.Unmarshal([]byte(s), ...) würde den String erst nach []byte kopieren; mit dem Reader sparen wir diesen Schritt.
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
Verbose bool `json:"verbose"`
}
func main() {
payload := `{"host":"mibeon.de","port":443,"verbose":true}`
var cfg Config
dec := json.NewDecoder(strings.NewReader(payload))
dec.DisallowUnknownFields()
if err := dec.Decode(&cfg); err != nil {
fmt.Println("Fehler:", err)
return
}
fmt.Printf("%+v\n", cfg)
}{Host:mibeon.de Port:443 Verbose:true}Der Vorteil gegenüber json.Unmarshal zeigt sich, sobald man Decoder-Optionen wie DisallowUnknownFields oder UseNumber braucht — die gibt es nur über den Decoder-Pfad. Genau dann ist strings.NewReader die natürliche Brücke.
HTTP-Body zweimal verwenden
In einem HTTP-Client möchte man manchmal denselben Request-Body mehrfach senden — etwa für Retries oder für ein doppeltes Hashing (Signatur + Übertragung). http.NewRequest akzeptiert einen io.Reader; wenn der Body ein *strings.Reader ist, erkennt das Standard-http-Paket das Seeker-Interface und kann den Body bei Retries automatisch zurücksetzen.
package main
import (
"crypto/sha256"
"fmt"
"io"
"strings"
)
func main() {
body := strings.NewReader(`{"event":"login","user":"michael"}`)
// 1) Hash über den Body bilden (für Signatur)
h := sha256.New()
if _, err := io.Copy(h, body); err != nil {
fmt.Println(err)
return
}
fmt.Printf("sha256=%x\n", h.Sum(nil))
// 2) Body zurücksetzen, damit der HTTP-Client ihn senden kann
if _, err := body.Seek(0, io.SeekStart); err != nil {
fmt.Println(err)
return
}
// 3) Body erneut konsumieren (simulierter Send)
sent, _ := io.ReadAll(body)
fmt.Printf("gesendet=%q\n", sent)
}sha256=4d7d0e4b3a2c8c9f1b0e8a2f6c1d3b5a7e9f0c2d4b6a8c0e2f4d6b8a0c2e4d6b
gesendet="{\"event\":\"login\",\"user\":\"michael\"}"Ohne Seek müsste man entweder den String zweimal in einen Reader packen oder den Body in eine Byte-Slice zwischenspeichern. Beides funktioniert, aber beides allokiert unnötig — der Seek-Pfad ist sauberer und schneller. (Der gezeigte SHA-256-Hash ist illustrativ.)
NewReader kopiert den String nicht
Der Reader hält intern nur einen Verweis auf den ursprünglichen String plus einen Offset. Egal wie groß der String — die Konstruktion ist O(1) und allokiert nur den Reader-Struct selbst.
Implementiert acht io-Interfaces
io.Reader, io.ReaderAt, io.ByteReader, io.ByteScanner, io.RuneReader, io.RuneScanner, io.Seeker und io.WriterTo — praktisch jeder lesende io-Kontrakt ist abgedeckt.
Seek(0, io.SeekStart) erlaubt Mehrfach-Lesen
Statt einen neuen Reader anzulegen, kann man die Position auf 0 zurücksetzen. Das HTTP-Standard-Paket nutzt genau dieses Muster für Retries.
Reset(s) für Pool-Wiederverwendung
In Hot-Paths lässt sich derselbe *strings.Reader über sync.Pool recyclen — Reset bindet ihn allokationsfrei an einen neuen String.
Schlanker als bytes.NewBufferString bei Read-only
bytes.NewBufferString kopiert den String in ein neues []byte; strings.NewReader nicht. Für reine Lese-Szenarien ist der strings.Reader immer die billigere Wahl.
ReadAt ist threadsafe
Mehrere Goroutines können parallel mit unterschiedlichen Offsets aus demselben Reader lesen, weil ReadAt den internen Zustand nicht mutiert. Read und Seek tun das hingegen sehr wohl.
Len() zeigt die verbleibenden Bytes
Nicht die Gesamtlänge, sondern den noch ungelesenen Rest. Praktisch für Fortschrittsanzeigen oder Größen-Checks vor einem Read.
Size() liefert die ursprüngliche Länge
Konstant über die Lebensdauer des Readers und immun gegen Seek/Read. Wird unter anderem von io.Copy ausgewertet, um Ziel-Puffer korrekt zu dimensionieren.
Weiterführende Ressourcen
Externe Quellen
strings.Readerio.ReaderInterfacebytes.NewBufferStringals Vergleich