fmt.Fscanln ist die zeilenorientierte Schwester von fmt.Fscan: Sie liest aus einem beliebigen io.Reader — Datei, Netzwerk-Verbindung, os.Stdin, strings.Reader — und stoppt am ersten Newline. Jeder Aufruf konsumiert genau eine Zeile aus dem Stream; was danach kommt, bleibt für den nächsten Aufruf im Puffer. Damit ist Fscanln das passende Werkzeug, wenn ein zeilenweise strukturierter Eingabestrom existiert und jede Zeile dieselbe, feste Anzahl Tokens enthält. Die offizielle Referenz steht auf pkg.go.dev/fmt#Fscanln.
Die Signatur im Detail
fmt.Fscanln folgt dem gleichen Schema wie die anderen Reader-Varianten der Fscan-Familie. Der erste Parameter ist die Eingabequelle, die restlichen sind Pointer auf die Zielvariablen, in welche die geparsten Tokens geschrieben werden.
func Fscanln(r io.Reader, a ...any) (n int, err error)Der Rückgabewert n zählt die erfolgreich gefüllten Argumente, err meldet Format-, Konvertierungs- oder Stream-Fehler. Am Ende des Streams kommt io.EOF zurück — das ist kein Bug, sondern das vereinbarte Ende-Signal.
Pro Aufruf genau eine Zeile
Der entscheidende Unterschied zu Fscan: Fscanln verbraucht den Newline am Zeilenende selbst und positioniert den internen Lesezeiger direkt hinter ihn. Mehrere aufeinanderfolgende Aufrufe lesen damit zuverlässig aufeinanderfolgende Zeilen — ohne dass du selbst Trennzeichen jonglieren musst.
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("alice 30\nbob 25\ncarol 41\n")
var name string
var age int
for {
_, err := fmt.Fscanln(r, &name, &age)
if err != nil {
break
}
fmt.Printf("%s ist %d Jahre alt\n", name, age)
}
}alice ist 30 Jahre alt
bob ist 25 Jahre alt
carol ist 41 Jahre altDrei Zeilen Eingabe, drei Iterationen, drei Datensätze — sauber zerlegt ohne manuelles Splitten. Die Schleife terminiert, sobald Fscanln io.EOF liefert.
Extra Tokens auf der Zeile sind ein Fehler
Fscanln ist bewusst streng: Findet sich nach dem letzten erwarteten Argument noch ein weiteres Token vor dem Newline, scheitert der Aufruf. Das schützt vor stillem Datenverlust, ist aber auch der Hauptgrund, warum Fscanln in der Praxis oft zu rigide wirkt.
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReader("alice 30 extra\n")
var name string
var age int
n, err := fmt.Fscanln(r, &name, &age)
fmt.Printf("n=%d err=%v\n", n, err)
}n=2 err=expected newlineBeide Argumente wurden gefüllt (n=2), aber der überzählige Token extra vor dem Newline lässt den Aufruf mit expected newline scheitern. Wer das nicht will, braucht ein toleranteres Verfahren.
Pointer-Pflicht: immer &var
Wie alle Scan-Funktionen schreibt Fscanln direkt in den Speicher der Zielvariablen. Ohne & würde nur eine Kopie befüllt — die Compiler-Prüfung verhindert das schon zur Übersetzungszeit, sobald die Argumente keine Pointer sind.
var n int
var s string
fmt.Fscanln(r, &n, &s) // korrekt: Adressen übergeben
// fmt.Fscanln(r, n, s) // Laufzeitfehler: keine PointerFscan vs. Fscanln im Direktvergleich
Beide Funktionen lesen aus demselben io.Reader, behandeln Newlines aber fundamental unterschiedlich. Die Tabelle fasst zusammen, wann welche Variante passt.
| Aspekt | Fscan | Fscanln |
|---|---|---|
| Newline | wird wie Whitespace behandelt | terminiert den Aufruf |
| Mehrzeilige Eingabe | ein Aufruf liest über Zeilengrenzen hinweg | ein Aufruf = eine Zeile |
Extra Token vor \n | irrelevant | Fehler expected newline |
| Typischer Einsatz | freies Token-Schlucken | strukturierte Zeilen mit fester Token-Anzahl |
| Fehlerbild bei zu wenigen Tokens | unexpected newline | unexpected newline |
Fscanln ist also der explizite Vertrag „pro Zeile genau diese Felder" — Fscan ist der Streamer.
TSV-Datei zeilenweise parsen
Ein klassischer Einsatzfall: eine tabulatorgetrennte Datei mit fester Spaltenanzahl. Solange jede Zeile garantiert die erwarteten Felder enthält, ist Fscanln die kürzeste sinnvolle Lösung — keine Scanner-Boilerplate, keine manuelle Tokenisierung.
package main
import (
"fmt"
"io"
"strings"
)
func main() {
data := "alice 87 active\nbob 42 inactive\ncarol 95 active\n"
r := strings.NewReader(data)
var name, status string
var score int
for {
_, err := fmt.Fscanln(r, &name, &score, &status)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Parse-Fehler:", err)
break
}
fmt.Printf("%-6s score=%d (%s)\n", name, score, status)
}
}alice score=87 (active)
bob score=42 (inactive)
carol score=95 (active)Drei Felder pro Zeile, drei Pointer-Argumente, EOF beendet die Schleife. Genau dafür ist Fscanln gebaut — und wenn die Eingabe wirklich so diszipliniert ist wie hier, gibt es kaum eine kompaktere Variante.
Why-not-Fscanln: Scanner + Fields ist robuster
Sobald die Token-Anzahl pro Zeile variiert oder die Eingabe nicht garantiert wohlgeformt ist, kippt Fscanln ins Negative. Jeder zusätzliche Token erzeugt expected newline; fehlende Tokens erzeugen unexpected newline. In realer Code-Basis ist das schmerzhaft, weil ein einziger Tippfehler im Input die ganze Verarbeitung kippt.
Der idiomatische Go-Weg ist daher: bufio.Scanner liefert die Zeile als String, strings.Fields zerlegt sie in Tokens, und die Konvertierung passiert kontrolliert mit strconv.
package main
import (
"bufio"
"fmt"
"strconv"
"strings"
)
func main() {
data := "alice 87 active\nbob 42\ncarol 95 active extra\n"
sc := bufio.NewScanner(strings.NewReader(data))
for sc.Scan() {
fields := strings.Fields(sc.Text())
if len(fields) < 2 {
fmt.Println("Zeile übersprungen:", sc.Text())
continue
}
name := fields[0]
score, err := strconv.Atoi(fields[1])
if err != nil {
fmt.Println("ungültiger Score:", fields[1])
continue
}
status := "unknown"
if len(fields) >= 3 {
status = fields[2]
}
fmt.Printf("%-6s score=%d (%s)\n", name, score, status)
}
}alice score=87 (active)
bob score=42 (unknown)
carol score=95 (active)Dieselbe Eingabe — aber statt zweimal in einen Fehler zu laufen, fängt die Schleife die unvollständige Bob-Zeile mit einem Default-Status ab und ignoriert das überzählige extra bei Carol. Für produktive Code-Pfade ist das der robustere Default; Fscanln bleibt elegant für sauber definierte interne Formate.
Reader-Quelle mit Newline-Stopp
Fscanln liest aus jedem io.Reader — Datei, Netzverbindung, strings.Reader, os.Stdin — und stoppt deterministisch am ersten Newline.
Pro Aufruf genau eine Zeile
Mehrere Fscanln-Aufrufe konsumieren aufeinanderfolgende Zeilen; der Newline selbst wird mit verbraucht, der Lesezeiger steht danach am Anfang der nächsten Zeile.
Strict-Mode: extra Tokens = Fehler
Findet sich nach den erwarteten Argumenten noch ein Token vor dem Newline, scheitert der Aufruf mit expected newline — selbst wenn alle Pointer befüllt wurden.
Pointer-Pflicht
Argumente müssen Adressen sein, also &name, &score. Ohne & schlägt der Aufruf zur Laufzeit fehl, weil Fscanln direkt in den Speicher schreibt.
bufio.Scanner ist robuster
Bei variabler Token-Anzahl oder unsicherer Eingabe ist bufio.Scanner.Text() + strings.Fields + strconv der idiomatische Weg — toleranter, debugbarer, ohne strikte Newline-Falle.
EOF ist kein Fehler
io.EOF signalisiert das saubere Ende des Streams. In Schleifen explizit gegen io.EOF prüfen und die Iteration beenden, statt EOF wie einen Parse-Fehler zu behandeln.