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.