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.

Go new_reader.go
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)
}
Output
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.

InterfaceMethode(n)Zweck
io.ReaderRead(p []byte)Standard-Lesepfad
io.ReaderAtReadAt(p []byte, off int64)Random-Access ohne State-Mutation
io.ByteReaderReadByte()byte-weiser Zugriff
io.ByteScannerUnreadByte()Lookahead um ein Byte
io.RuneReaderReadRune()UTF-8-dekodierter Zugriff
io.RuneScannerUnreadRune()Lookahead um eine Rune
io.SeekerSeek(off, whence)Position ändern
io.WriterToWriteTo(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).

Go read_progress.go
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
		}
	}
}
Output
chunk="ABCD" verbleibend=6
chunk="EFGH" verbleibend=2
chunk="IJ" verbleibend=0
EOF erreicht

Beachte 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.

Go seek.go
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)
}
Output
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.

Go byte_rune.go
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)
	}
}
Output
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.

Go read_at.go
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())
}
Output
ab Offset 10: "ABCD" (n=4)
ab Offset  2: "2345" (n=4)
Len() unverändert: 16

Im 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.

Go reset.go
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"))
}
Output
erste Anfrage
zweite Anfrage
dritte Anfrage

Im 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.

Aspektstrings.NewReader(s)bytes.NewBufferString(s)
Mutationread-onlyread-write
String-Kopienein (nur Verweis)ja (kopiert in []byte)
Allokation~24 Byte~24 Byte + len(s)
io.Seekerjanein
io.ReaderAtjanein
ResetReset(string)Reset() (leert)
Threadsafe-ReadsReadAt ja, Read neinnein

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.

Go json_string.go
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)
}
Output
{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.

Go seek_retry.go
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)
}
Output
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

/ Weiter

Zurück zu Das strings-Paket — String-Manipulation

Zur Übersicht