fmt.Fscan ist die allgemeinste Form der Scan-Familie: Sie liest aus einem beliebigen io.Reader und parst dort whitespace-getrennte Tokens in die übergebenen Pointer-Argumente. Während Scan fest an os.Stdin hängt und Sscan einen fertigen String erwartet, ist Fscan die Variante, die mit Dateien, Network-Verbindungen, bytes.Buffer oder jeder anderen Reader-Implementierung zusammenarbeitet. Damit eignet sich Fscan für Stream-basiertes Parsing — überall dort, wo Daten nicht als ein Stück im Speicher liegen, sondern stückweise eintrudeln. Referenz: pkg.go.dev/fmt#Fscan.

Funktionssignatur

Die Signatur unterscheidet sich von Scan nur durch das erste Argument:

Go signatur.go
func Fscan(r io.Reader, a ...any) (n int, err error)

Der io.Reader ist die Datenquelle, die variadischen any-Argumente sind die Ziel-Pointer. n zählt erfolgreich gescannte Werte, err liefert Parse- oder I/O-Fehler. Da io.Reader ein Interface ist, akzeptiert Fscan alles, was Read([]byte) (int, error) implementiert — also *os.File, net.Conn, *bytes.Buffer, *strings.Reader, http.Response.Body und unzählige weitere Typen.

Whitespace trennt Tokens

Fscan verhält sich beim Parsing identisch zu Scan: Whitespace — Leerzeichen, Tabs, Newlines — trennt Tokens, und Newlines haben keine Sonderbedeutung. Drei Werte in drei Zeilen oder in einer Zeile sind aus Sicht von Fscan dasselbe.

Go whitespace.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	r := strings.NewReader("Anna\n42\n1.75\n")
	var name string
	var age int
	var height float64
	n, err := fmt.Fscan(r, &name, &age, &height)
	fmt.Printf("n=%d err=%v\n", n, err)
	fmt.Printf("%s, %d Jahre, %.2f m\n", name, age, height)
}
Output
n=3 err=<nil>
Anna, 42 Jahre, 1.75 m

strings.NewReader dient hier als minimaler io.Reader für das Beispiel. In realen Programmen kommt der Reader meist von os.Open, net.Dial oder einer HTTP-Response. Beachte: Obwohl die Tokens durch Newlines getrennt sind, sieht Fscan nur „whitespace" — derselbe Aufruf würde auch bei "Anna 42 1.75" in einer einzigen Zeile funktionieren.

Aus einer Datei lesen

Der klassische Einsatz von Fscan ist das zeilen- oder tokenweise Lesen aus einer Datei. os.Open liefert ein *os.File, das io.Reader erfüllt; danach liest eine Schleife so lange, bis ein Fehler — typischerweise io.EOF — das Ende signalisiert.

Go datei.go
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// scores.txt enthält:
	//   Anna 87
	//   Ben 92
	//   Clara 78
	file, err := os.Open("scores.txt")
	if err != nil {
		fmt.Println("open:", err)
		return
	}
	defer file.Close()

	for {
		var name string
		var score int
		_, err := fmt.Fscan(file, &name, &score)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("parse:", err)
			break
		}
		fmt.Printf("%-6s %d\n", name, score)
	}
}
Output
Anna   87
Ben    92
Clara  78

Jeder Schleifendurchlauf konsumiert genau zwei Tokens. Fscan merkt sich intern (über einen kleinen Puffer am Reader), wo es zuletzt aufgehört hat, und macht dort weiter. Das Beispiel zeigt auch die übliche Fehlerstruktur: io.EOF ist das normale Ende und bricht still ab, alles andere ist ein echter Fehler.

Mit einem net.Conn als Reader

net.Conn implementiert ebenfalls io.Reader, also kann man theoretisch direkt aus einem TCP-Stream parsen. In der Praxis ist das selten ratsam, aber lehrreich:

Go network.go
package main

import (
	"fmt"
	"net"
)

func handle(conn net.Conn) {
	defer conn.Close()
	var command string
	var arg int
	// Erwartet z. B. "PING 5\n" vom Client.
	if _, err := fmt.Fscan(conn, &command, &arg); err != nil {
		fmt.Println("parse:", err)
		return
	}
	fmt.Printf("Befehl %q mit Argument %d\n", command, arg)
}

func main() {
	ln, _ := net.Listen("tcp", ":9000")
	for {
		conn, _ := ln.Accept()
		go handle(conn)
	}
}

Funktioniert für triviale Protokolle, hat aber gravierende Nachteile: Fscan blockiert, bis genug Tokens eingetroffen sind, kennt keine Timeouts auf Token-Ebene, puffert Daten intern (was späteres conn.Read durcheinanderbringt) und verarbeitet keine Framing-Logik. Für ernstzunehmende Netzwerk-Protokolle nimmt man bufio.Scanner, bufio.Reader.ReadString('\n') oder einen echten Protokoll-Parser.

Vergleich: Scan vs. Sscan vs. Fscan

Alle drei Funktionen parsen identisch — der Unterschied ist allein die Quelle der Daten:

FunktionQuelleTyp der QuelleTypischer Einsatz
fmt.Scanos.Stdin (fix)*os.FileInteraktive CLI-Eingabe
fmt.SscanStringstringBereits geladene Texte, Tests, Konfig-Zeilen
fmt.Fscanbeliebigio.ReaderDateien, Streams, Network, Buffer

Scan(&x) ist exakt äquivalent zu Fscan(os.Stdin, &x), und Sscan(s, &x) ist äquivalent zu Fscan(strings.NewReader(s), &x). Die Familie ist also intern um Fscan herum gebaut; die anderen sind Bequemlichkeits-Wrapper.

Pointer sind Pflicht

Wie bei der gesamten Scan-Familie müssen alle variadischen Argumente Pointer sein. Wer den Wert direkt übergibt, bekommt zur Laufzeit den klassischen Fehler:

Go pointer.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	r := strings.NewReader("42")
	var n int
	_, err := fmt.Fscan(r, n) // BUG: Wert statt Pointer
	fmt.Println("err:", err)
}
Output
err: can't scan type: int

Der Grund liegt in Go's Wertesemantik: Ohne Pointer hätte Fscan nur eine Kopie und könnte die ursprüngliche Variable nicht beschreiben. Das &-Symbol vor jedem Argument ist nicht Stil-Kosmetik, sondern notwendig.

Praktische Stolperfallen

Drei Eigenheiten machen Fscan in Produktionscode tückisch. Erstens blockiert der Aufruf, bis genug Tokens vorliegen — bei einer Pipe oder Network-Verbindung kann das beliebig lange dauern, und es gibt keine Token-Timeout-API. Wer Deadlines braucht, setzt conn.SetReadDeadline oder kapselt den Reader in eine eigene Struktur.

Zweitens ist das Mischen mit anderem Reading auf demselben Reader unzuverlässig. Fscan puffert intern (typischerweise ein paar Bytes voraus, um Token-Grenzen zu erkennen). Wenn anschließend file.Read direkt aufgerufen wird, fehlen genau diese Bytes. Faustregel: Ein Reader gehört entweder ganz Fscan oder ganz einem anderen Mechanismus.

Drittens muss man bei n und err aufpassen. Fscan(r, &a, &b, &c) kann mit n=2, err=... zurückkehren — zwei Werte wurden gelesen, beim dritten ging es schief. a und b sind gültig, c enthält noch den vorherigen Wert. Bei kritischem Code prüft man immer beide Rückgaben.

Praxis 1: Zwei-Spalten-Datei strukturiert lesen

Klassisches Szenario: eine Datenei mit Name Punktzahl pro Zeile, die als Liste eingelesen werden soll. Fscan in einer Schleife ist hier kompakt und idiomatisch — solange das Format streng eingehalten wird.

Go praxis-datei.go
package main

import (
	"fmt"
	"io"
	"os"
	"sort"
)

type Score struct {
	Name   string
	Points int
}

func loadScores(path string) ([]Score, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var scores []Score
	for {
		var s Score
		_, err := fmt.Fscan(file, &s.Name, &s.Points)
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("zeile %d: %w", len(scores)+1, err)
		}
		scores = append(scores, s)
	}
	return scores, nil
}

func main() {
	scores, err := loadScores("scores.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	sort.Slice(scores, func(i, j int) bool {
		return scores[i].Points > scores[j].Points
	})
	for i, s := range scores {
		fmt.Printf("%d. %-8s %3d\n", i+1, s.Name, s.Points)
	}
}
Output
1. Ben       92
2. Anna      87
3. Clara     78

Die Struktur ist sauber: Fscan liest jeweils ein Name-Points-Paar, die Schleife terminiert sauber bei io.EOF, andere Fehler werden mit Zeilennummer angereichert weitergereicht. So lange das Format diszipliniert ist — keine Leerzeilen, keine Kommentare, keine Header — funktioniert das hervorragend. Sobald die Datei flexibler wird, kippt der Vorteil von Fscan schnell.

Praxis 2: Warum bufio.Scanner oft besser ist

Sobald reale Daten ins Spiel kommen — Header-Zeilen, Kommentare, variable Spaltenzahl, fehlertolerantes Parsen — wird Fscan schnell zur Bremse. Der robustere Weg ist bufio.Scanner mit Zeilen-Granularität plus strings.Fields für die Token-Aufteilung:

Go praxis-scanner.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func loadScores(path string) ([]Score, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var scores []Score
	scanner := bufio.NewScanner(file)
	lineNo := 0
	for scanner.Scan() {
		lineNo++
		line := strings.TrimSpace(scanner.Text())
		// Leerzeilen und Kommentare überspringen — mit Fscan unmöglich.
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) != 2 {
			return nil, fmt.Errorf("zeile %d: erwarte 2 felder, fand %d", lineNo, len(fields))
		}
		points, err := strconv.Atoi(fields[1])
		if err != nil {
			return nil, fmt.Errorf("zeile %d: %w", lineNo, err)
		}
		scores = append(scores, Score{Name: fields[0], Points: points})
	}
	return scores, scanner.Err()
}

type Score struct {
	Name   string
	Points int
}

func main() {
	scores, err := loadScores("scores.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, s := range scores {
		fmt.Printf("%-8s %d\n", s.Name, s.Points)
	}
}
Output
Anna     87
Ben      92
Clara    78

Der Scanner-Weg ist länger, aber jeder einzelne Schritt ist explizit und kontrollierbar: Zeile lesen, trimmen, Kommentar prüfen, Felder zählen, einzeln konvertieren, mit Zeilennummer fehlerannotieren. Genau diese Granularität fehlt bei Fscan. Faustregel: Fscan für Quick-Scripts, kontrollierte Eingaben und Algorithmus-Aufgaben; bufio.Scanner für alles, was länger als eine Woche im Repository bleibt.

Erkenntnisse

io.Reader als erstes Argument

Anders als bei Scan/Sscan steht bei Fscan der Reader explizit als erstes Argument — alle Pointer folgen danach.

Pointer-Pflicht

Alle Ziel-Argumente müssen Pointer sein; sonst can't scan type: ... zur Laufzeit, nicht zur Compile-Zeit.

Whitespace inkl. Newlines trennt Tokens

Leerzeichen, Tabs und Newlines wirken identisch — Zeilenstruktur ist für Fscan unsichtbar.

Blockiert bis genug Tokens da sind

Auf langsamen Streams (Pipes, Network) wartet Fscan ohne Timeout-API; ggf. Reader-Deadlines setzen.

Pufferung kollidiert mit Mixed-Reading

Fscan puffert intern voraus; anschließendes direktes Read auf demselben Reader verliert Bytes.

bufio.Scanner für Production

Für robustes Datei- oder Stream-Parsing mit Kommentaren, Leerzeilen und Fehlertoleranz ist bufio.Scanner + strings.Fields der bessere Default.

Funktioniert mit jedem io.Reader

*os.File, net.Conn, *bytes.Buffer, *strings.Reader, http.Response.Body — alles funktioniert ohne Adapter.

n und err gemeinsam prüfen

Bei Partial-Reads ist n < len(args) und err != nil; nur die ersten n Werte sind verlässlich.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht