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:

Go signatur.go
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:

Go records.go
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)
	}
}
Output
Alice  -> 30 Jahre
Bob    -> 25 Jahre
Carol  -> 42 Jahre

Die 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:

Go whitespace.go
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)
}
Output
2 <nil> 42 99

Mehrere 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:

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

Der 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:

Go praxis_fscanf.go
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)
	}
}
Output
WARN  auth   token_expired
INFO  http   request_ok
ERROR db     connection_lost

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

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

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Das fmt-Paket — Formatierte I/O

Zur Übersicht