fmt.Fscanf ist die dritte Ecke des Scanf-Dreiecks: Quelle ist wie bei Fscan ein beliebiger io.Reader, die Kontrolle übernimmt — wie bei Scanf und Sscanf — ein expliziter Format-String. Damit lässt sich strukturierter Input direkt aus einer Datei, einer Netzwerk-Verbindung oder einem bytes.Buffer parsen, ohne den gesamten Inhalt vorher in einen String zu materialisieren. Die kanonische Referenz ist pkg.go.dev/fmt#Fscanf.
In der Praxis ist Fscanf ein Werkzeug für sehr regelmäßige Stream-Formate. Sobald Fehlertoleranz, Logging fehlerhafter Zeilen oder strukturiertes Recovery gefragt sind, ist die Kombination bufio.Scanner + fmt.Sscanf fast immer die bessere Architektur — diese Seite zeigt beide Wege und macht den Unterschied greifbar.
Signatur und Rückgabewerte
Fscanf liest aus einem io.Reader, interpretiert den Inhalt nach einem Format-String und schreibt die geparsten Werte in die übergebenen Pointer:
func Fscanf(r io.Reader, format string, a ...any) (n int, err error)Der Rückgabewert n zählt die erfolgreich gefüllten Argumente — nicht die gelesenen Bytes. Das ist wichtig: nach einem Teilerfolg weiß der Aufrufer genau, wie weit der Parser gekommen ist, und kann gezielt reagieren. Der Fehler io.EOF signalisiert das saubere Ende des Streams; jeder andere Fehler deutet auf einen Format-Mismatch oder einen I/O-Fehler hin.
Strukturierte Records aus einer Datei
Der klassische Anwendungsfall: eine Datei, in der jede Zeile demselben starren Schema folgt. Hier Name=<wort> Age=<zahl> — ein Format ohne Varianz, ohne optionale Felder, ohne quoting. Genau das, wofür Format-String-Parsing gemacht ist:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
data := `Name=Alice Age=30
Name=Bob Age=25
Name=Carol Age=42
`
r := strings.NewReader(data)
for {
var name string
var age int
_, err := fmt.Fscanf(r, "Name=%s Age=%d\n", &name, &age)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Parse-Fehler:", err)
break
}
fmt.Printf("%-6s -> %d Jahre\n", name, age)
}
}Alice -> 30 Jahre
Bob -> 25 Jahre
Carol -> 42 JahreDie Schleife läuft, bis Fscanf io.EOF zurückgibt. Jeder andere Fehler wird sichtbar protokolliert. Der Reader ist hier ein strings.NewReader, kann aber genauso gut ein *os.File, ein net.Conn oder ein bufio.Reader sein — die Funktion sieht den Unterschied nicht.
Welche Verben gelten
Die Verben sind identisch mit Scanf und Sscanf — die Familie teilt sich denselben Parser. Eine Kurzübersicht:
Wichtigste Scan-Verben
%d— Dezimalzahl (int-Familie)%f— Float%s— Whitespace-getrenntes Wort (kein quoting!)%q— Go-quoted String%c— einzelnes Rune%x%o%b— Hex, Oktal, Binär%v— generisch, je nach Pointer-Typ
Die vollständige Tabelle mit allen Feinheiten steht auf der Referenzseite Format-Verben.
Whitespace im Format ist flexibel
Wie bei den anderen Scan-Funktionen matched ein einzelnes Leerzeichen im Format-String beliebig viel Whitespace im Input — inklusive Tabs und Zeilenumbrüchen (außer bei Fscanln). Das ist bequem, kann aber auch Stolperfalle sein:
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("x=42 y=99")
var x, y int
n, err := fmt.Fscanf(r, "x=%d y=%d", &x, &y)
fmt.Println(n, err, x, y)
}2 <nil> 42 99Mehrere Leerzeichen zwischen x=42 und y=99 stören nicht — der Parser frisst sie alle. Wer eine exakte Trennung erzwingen will, muss zu anderen Werkzeugen greifen; Format-Strings sind dafür das falsche Werkzeug.
Pointer-Pflicht
Selbstverständlich gilt auch hier: alle Ziel-Argumente sind Pointer. Wer einen Wert statt einer Adresse übergibt, bekommt zur Laufzeit einen Fehler — der Compiler kann es nicht prüfen, weil die Variadics als any durchgereicht werden:
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("count=7")
var count int
fmt.Fscanf(r, "count=%d", count) // FALSCH: kein Pointer
fmt.Println(count)
}0Der Fehler wird vom Returnwert (err) signalisiert; wer ihn ignoriert, sieht still den Nullwert. Deshalb: err immer prüfen, gerade beim Lernen.
Wann besser bufio.Scanner + Sscanf
Die größte Schwäche von Fscanf ist die Fehlerbehandlung mitten im Stream. Sobald eine einzige Zeile vom erwarteten Format abweicht, hängt der Parser an der Fehlerstelle fest — der Reader-Cursor steht irgendwo, der Folgeaufruf scheitert oft erneut, und es gibt keine saubere Möglichkeit, „bis zur nächsten Zeile zu überspringen".
Die idiomatische Lösung in Go: bufio.Scanner segmentiert den Stream zeilenweise, und fmt.Sscanf parst jede Zeile als unabhängigen String. Vorteile dieser Trennung:
- Eine fehlerhafte Zeile bricht nur diesen einen
Sscanf-Aufruf — die Schleife läuft weiter. - Fehlerhafte Zeilen lassen sich loggen, zählen, in eine Reject-Datei schreiben.
- Der Scanner-Cursor steht nach jedem Durchlauf garantiert am Zeilenanfang.
- Tests werden einfacher: man parst Strings, keine Reader-Mocks.
Für streng formatierte, vertrauenswürdige Streams (etwa selbst erzeugte Protokolldateien zwischen eigenen Systemen) bleibt Fscanf kompakt und idiomatisch. Sobald Input aus fremder Hand kommt — Konfigurationen, Logs anderer Programme, Netzwerk-Payloads — gewinnt der Scanner-Ansatz.
Strukturierte Logzeilen mit Fscanf parsen
Eine realistische Situation: ein kleiner Service schreibt strukturierte Logs in einem festen Schema. Datum in eckigen Klammern, Level, dann module=<name> und msg=<wort>. Solange das Format stabil ist, ist Fscanf ein direkter, lesbarer Weg:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
log := `[2026-05-22] WARN module=auth msg=token_expired
[2026-05-22] INFO module=http msg=request_ok
[2026-05-22] ERROR module=db msg=connection_lost
`
r := strings.NewReader(log)
for {
var date, level, module, msg string
_, err := fmt.Fscanf(r,
"[%s %s module=%s msg=%s\n",
&date, &level, &module, &msg,
)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Parse-Abbruch:", err)
break
}
// %s liest bis Whitespace; das schließende ']' bleibt am date hängen.
date = strings.TrimSuffix(date, "]")
fmt.Printf("%-5s %-6s %s\n", level, module, msg)
}
}WARN auth token_expired
INFO http request_ok
ERROR db connection_lostBeachte das kleine Detail mit der schließenden eckigen Klammer: %s liest bis zum nächsten Whitespace und nimmt das ] mit. Das muss anschließend von Hand getrimmt werden. Genau diese Art von Mikro-Reibung ist es, die Fscanf in der Praxis spröde macht — der Parser kennt keine Trennzeichen außer Whitespace.
Dieselbe Datei mit bufio.Scanner + Sscanf
Derselbe Input, derselbe Output — aber mit einer Architektur, die einzelne kaputte Zeilen verzeiht:
package main
import (
"bufio"
"fmt"
"strings"
)
func main() {
log := `[2026-05-22] WARN module=auth msg=token_expired
[2026-05-22] INFO module=http msg=request_ok
GARBAGE-ZEILE-OHNE-FORMAT
[2026-05-22] ERROR module=db msg=connection_lost
`
scanner := bufio.NewScanner(strings.NewReader(log))
rejected := 0
for scanner.Scan() {
line := scanner.Text()
var date, level, module, msg string
_, err := fmt.Sscanf(line,
"[%s %s module=%s msg=%s",
&date, &level, &module, &msg,
)
if err != nil {
rejected++
continue // diese Zeile überspringen, weitermachen
}
date = strings.TrimSuffix(date, "]")
fmt.Printf("%-5s %-6s %s\n", level, module, msg)
}
fmt.Printf("(%d Zeilen verworfen)\n", rejected)
}WARN auth token_expired
INFO http request_ok
ERROR db connection_lost
(1 Zeilen verworfen)Die Garbage-Zeile mittendrin bringt die Pipeline nicht aus dem Tritt — sie wird gezählt, übersprungen, und der Rest läuft sauber durch. Diese Robustheit ist der Grund, warum erfahrene Go-Entwickler diesen Ansatz fast reflexhaft wählen. Fscanf direkt auf dem Reader hat seinen Platz, aber er ist enger als man zunächst denkt.
Reader-Quelle + Format-String
Fscanf kombiniert die Stream-Fähigkeit von Fscan mit der expliziten Format-Kontrolle von Scanf — Parser direkt auf io.Reader, ohne Zwischen-String.
Pointer-Pflicht
Alle Ziel-Argumente sind Adressen (&x). Der Compiler kann es wegen der any-Variadics nicht prüfen — der Fehler kommt erst zur Laufzeit über den err-Rückgabewert.
Whitespace flexibel
Ein Leerzeichen im Format matched beliebig viel Whitespace im Input. Praktisch, aber kein Werkzeug für strikte Trennzeichen-Validierung.
Verben wie Scanf
%d, %f, %s, %q, %v und Konsorten verhalten sich exakt wie bei Scanf/Sscanf — eine geteilte Verb-Familie.
Robustere Alternative
bufio.Scanner segmentiert zeilenweise, fmt.Sscanf parst jede Zeile isoliert. Fehlerhafte Zeilen brechen nicht den ganzen Stream.
Mischen mit anderem Reading riskant
Wer auf demselben io.Reader Fscanf mit Read/ReadString mischt, riskiert verlorene Bytes durch interne Pufferung — eine Quelle abgrundtiefer Bugs.
EOF ist kein Fehler
io.EOF signalisiert das saubere Ende. In der Schleife immer separat prüfen, bevor andere Fehler protokolliert werden.
Sweet Spot: simple, vertrauenswürdige Streams
Für selbst erzeugte, streng formatierte Protokoll-Streams zwischen eigenen Systemen ist Fscanf kompakt und idiomatisch. Bei fremdem Input lieber Scanner + Sscanf.