fmt.Scan ist das einfachste Werkzeug der Standardbibliothek, um Werte von der Standardeingabe (os.Stdin) einzulesen. Die Funktion liest whitespace-getrennte Tokens und parst sie der Reihe nach in die übergebenen Pointer-Argumente — ein Token pro Argument, egal ob die Tokens durch Leerzeichen, Tabs oder Zeilenumbrüche getrennt sind.

In der Praxis sieht man fmt.Scan selten in Production-Code. Sobald Eingaben unkontrolliert, mehrzeilig oder potenziell mit Sonderzeichen versehen sind, wird das Verhalten unübersichtlich. Für robustes Stdin-Handling greift man stattdessen zu bufio.Scanner mit explizitem strconv-Parsing. Diese Seite erklärt fmt.Scan trotzdem gründlich, weil es in Lehrbüchern, kleinen CLI-Tools und Coding-Challenges allgegenwärtig ist und das Verständnis der Mechanik die Grenzen klarer macht.

Signatur

Die Funktion akzeptiert eine variadische Liste von any-Werten — in der Praxis Pointer auf die Ziel-Variablen — und liefert die Anzahl erfolgreich gelesener Werte sowie einen error zurück. Die Pointer-Pflicht ist keine Konvention, sondern technische Notwendigkeit: fmt.Scan muss in fremde Variablen hineinschreiben, und in Go geht das nur über Adressen.

Der Rückgabewert n ist besonders wichtig, wenn nicht alle erwarteten Tokens vorhanden sind. Ein Aufruf, der drei Werte erwartet, kann mit n == 2 und einem Fehler zurückkommen — die ersten beiden Variablen sind dann gesetzt, die dritte nicht.

Go signature.go
// Aus dem fmt-Paket:
func Scan(a ...any) (n int, err error)

Whitespace als Trenner

fmt.Scan betrachtet jede Form von Whitespace als Token-Trenner: einfache Leerzeichen, Tabs und Zeilenumbrüche wirken gleichwertig. Mehrere aufeinanderfolgende Whitespace-Zeichen werden zusammengefasst, leere Tokens gibt es nicht. Das ist bequem für triviale Eingaben, wird aber zur Falle, sobald die Eingabe strukturiert sein soll — Scan ignoriert die Zeilenstruktur komplett.

Im folgenden Beispiel ist es für fmt.Scan irrelevant, ob die drei Werte in einer Zeile, in drei Zeilen oder gemischt eingegeben werden. Es liest weiter, bis alle drei Argumente befüllt sind.

Go whitespace.go
package main

import "fmt"

func main() {
	var a, b, c int
	n, err := fmt.Scan(&a, &b, &c)
	fmt.Printf("n=%d err=%v -> %d %d %d\n", n, err, a, b, c)
}
Output
$ echo "10  20
30" | go run whitespace.go
n=3 err=<nil> -> 10 20 30

Pointer-Pflicht

Wer einen Wert statt einer Adresse übergibt, bekommt keinen Compile-Fehler — die Signatur akzeptiert ...any, also wird auch ein int brav durchgereicht. Der Fehler entsteht zur Laufzeit, weil fmt.Scan via Reflection prüft, ob das Argument schreibbar ist. Ohne Pointer kann es nicht zurückschreiben und liefert einen Fehler wie type not a pointer.

Diese Schwäche ist typisch für reflection-basierte APIs: Die Typprüfung verschiebt sich vom Compiler auf den Programmstart. Das ist ein weiterer Grund, warum bufio.Scanner + strconv in Production oft bevorzugt wird — dort sind Fehler statisch sichtbar.

Go pointer.go
package main

import "fmt"

func main() {
	var x int
	// FALSCH: Wert statt Adresse
	n, err := fmt.Scan(x)
	fmt.Printf("n=%d err=%v\n", n, err)
}
Output
$ echo 42 | go run pointer.go
n=0 err=type not a pointer: int

Type-aware Parsing

fmt.Scan inspiziert zur Laufzeit den dynamischen Typ jedes Pointer-Arguments und wählt eine passende Parsing-Strategie: *int erwartet eine Dezimalzahl, *float64 eine Kommazahl, *string ein whitespace-getrenntes Token, *bool Strings wie true, false, 0, 1. Es gibt keine Konvertierungs-Magie — passt das Token nicht zum Zieltyp, bricht der Scan an dieser Stelle ab.

Wichtig ist, dass *string hier nur ein Token liest, nicht die ganze Zeile. Wer eine Eingabezeile mit Leerzeichen einlesen will (etwa einen Namen mit Vor- und Nachname), kann das mit fmt.Scan nicht direkt — das ist erneut ein Fall für bufio.Scanner.

Go types.go
package main

import "fmt"

func main() {
	var name string
	var alter int
	var groesse float64
	var aktiv bool

	n, err := fmt.Scan(&name, &alter, &groesse, &aktiv)
	fmt.Printf("n=%d err=%v\n", n, err)
	fmt.Printf("%s, %d Jahre, %.2fm, aktiv=%t\n", name, alter, groesse, aktiv)
}
Output
$ echo "Anna 29 1.72 true" | go run types.go
n=4 err=<nil>
Anna, 29 Jahre, 1.72m, aktiv=true

Fehlerverhalten

fmt.Scan kann auf zwei Arten scheitern. Erstens durch Parse-Fehler, wenn ein Token nicht zum Zieltyp passt — etwa "abc" für ein *int. Zweitens durch vorzeitiges Ende der Eingabe (io.EOF), wenn die Stdin schließt, bevor alle Argumente befüllt sind. In beiden Fällen wird n auf die Anzahl der bereits erfolgreich gelesenen Werte gesetzt — partielle Reads sind also der Normalfall, nicht die Ausnahme.

Das macht robuste Behandlung lästig: Nach jedem Scan muss man err prüfen und den Status der Variablen verstehen. Bei n < len(args) darf man die nicht-befüllten Variablen nicht verwenden, weil sie ihren Zero-Value behalten und das stillschweigend falsche Berechnungen ermöglicht.

Go errors.go
package main

import "fmt"

func main() {
	var a, b, c int
	n, err := fmt.Scan(&a, &b, &c)
	fmt.Printf("n=%d err=%v -> a=%d b=%d c=%d\n", n, err, a, b, c)
}
Output
$ echo "10 abc 30" | go run errors.go
n=1 err=expected integer -> a=10 b=0 c=0

Warum bufio.Scanner besser ist

fmt.Scan ist konzeptionell fragil. Es ignoriert Zeilenstruktur, was bei jeder Eingabe mit semantischer Bedeutung pro Zeile zum Problem wird. Es liest Strings nur token-weise, was Eingaben mit Leerzeichen unmöglich macht. Die Fehlerbehandlung verteilt Erfolg zwischen n und err, was idiomatischen Go-Stil verwässert. Und reflection-basierte Typprüfung verschiebt Fehler in die Laufzeit.

bufio.Scanner hingegen liest klar definierte Einheiten — standardmäßig Zeilen — und überlässt das Parsen explizit dem strconv-Paket. Die Verantwortung ist sauber getrennt: Scanner liest Bytes, strconv validiert und konvertiert. Das ist mehr Code, aber jedes Stück hat eine klare Bedeutung und ein klares Fehlerprofil.

Vergleich Scan / Scanln / Scanf

Die drei Stdin-lesenden Funktionen im fmt-Paket unterscheiden sich vor allem darin, wie sie Whitespace und Zeilenumbrüche behandeln. Wer das richtige Werkzeug wählt, spart sich Fehlersuche bei interaktiven Eingaben.

FunktionNewline-VerhaltenFormat-StringTypischer Einsatz
fmt.ScanNewlines sind normaler Whitespace, liest über Zeilen hinwegneinMehrere Werte auf einmal, Zeilenstruktur egal
fmt.ScanlnBricht am Newline ab, alle Argumente müssen in einer Zeile seinneinEine Zeile = ein Eingabesatz
fmt.ScanfFormat-String steuert Trenner und LayoutjaStrukturierte Eingaben mit festem Format

In der Praxis ist Scanln der häufigste Vertreter, weil interaktive Prompts üblicherweise pro Zeile abgearbeitet werden. Scan wird vor allem für Aufgaben gebraucht, bei denen mehrere Werte am Stück geliefert werden — etwa bei Stdin-Pipes oder Coding-Challenges.

Praxis 1 — interaktiver CLI-Prompt

Ein klassisches Einsatzgebiet ist ein kleines CLI-Tool, das zwei Zahlen abfragt und das Ergebnis berechnet. fmt.Scan reicht hier, weil die Eingabe einfach ist und der Benutzer beide Werte in beliebigem Whitespace-Layout angeben darf.

Beachte den Print vor dem Scan und das Println zur Strukturierung der Ausgabe — interaktive Tools fühlen sich erst rund an, wenn Prompt und Ausgabe klar getrennt sind. Bei Fehlern wird hier bewusst hart abgebrochen, weil ein interaktiver Nutzer den Befehl ohnehin neu absetzt.

Go praxis1.go
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Print("Zwei ganze Zahlen eingeben: ")
	var a, b int
	if _, err := fmt.Scan(&a, &b); err != nil {
		fmt.Fprintln(os.Stderr, "Eingabefehler:", err)
		os.Exit(1)
	}
	fmt.Printf("Summe: %d\n", a+b)
}
Output
$ go run praxis1.go
Zwei ganze Zahlen eingeben: 17 25
Summe: 42

Praxis 2 — Why-not-Scan mit bufio.Scanner

Sobald die Eingabe mehrzeilig oder potenziell mit Sonderzeichen versehen ist, wird bufio.Scanner die bessere Wahl. Das folgende Beispiel liest pro Zeile einen Datensatz „Name Alter" — der Name darf Leerzeichen enthalten, was fmt.Scan nicht leisten kann.

Die Verantwortlichkeiten sind sauber getrennt: bufio.Scanner produziert Zeilen, strings.LastIndex trennt Name von Zahl, strconv.Atoi validiert die Zahl. Jede Stufe hat einen klaren Fehlerpfad, und es gibt keinen partiellen Read auf Variablenebene.

Go praxis2.go
package main

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		idx := strings.LastIndex(line, " ")
		if idx == -1 {
			fmt.Fprintln(os.Stderr, "Format: <Name> <Alter>")
			continue
		}
		name := strings.TrimSpace(line[:idx])
		alter, err := strconv.Atoi(strings.TrimSpace(line[idx+1:]))
		if err != nil {
			fmt.Fprintf(os.Stderr, "Alter nicht parsbar: %v\n", err)
			continue
		}
		fmt.Printf("%-20s %3d\n", name, alter)
	}
	if err := scanner.Err(); err != nil {
		fmt.Fprintln(os.Stderr, "Scan-Fehler:", err)
		os.Exit(1)
	}
}
Output
$ printf "Anna Schmidt 29\nMax von Hagen 41\n" | go run praxis2.go
Anna Schmidt          29
Max von Hagen         41

Pointer-Pflicht

Jedes Argument an fmt.Scan muss eine Adresse sein. Ohne & gibt es keinen Compile-Fehler, sondern einen Laufzeitfehler type not a pointer. Das ist eine direkte Folge der reflection-basierten Implementierung.

Whitespace inkl. Newlines

fmt.Scan ignoriert Zeilenstruktur vollständig. Tabs, Leerzeichen und \n wirken alle als Token-Trenner — wer Zeilen unterscheiden will, braucht Scanln oder bufio.Scanner.

Production: bufio.Scanner

In echtem Code gehört Stdin-Lesen in die Hand von bufio.Scanner plus strconv. Saubere Trennung von Lesen und Parsen, statische Typprüfung, klare Fehlerpfade.

Partial-Reads möglich

Bei Fehlern oder vorzeitigem EOF kommt n kleiner als die Anzahl der Argumente zurück. Variablen jenseits von n behalten ihren Zero-Value — das ist eine klassische Quelle stiller Bugs.

Encoding-Probleme

fmt.Scan ist nicht für nicht-ASCII-Whitespace oder spezielle Encodings optimiert. Bei UTF-8-Eingaben mit ungewöhnlichen Zeichen wird das Verhalten schnell unvorhersehbar.

Type-Inspektion zur Laufzeit

Die Parsing-Strategie wird per Reflection am dynamischen Typ jedes Pointers gewählt. Das ist flexibel, aber verschiebt Typfehler vom Compiler auf den Programmstart.

Scanner-Interface für Custom-Types

Eigene Typen können das fmt.Scanner-Interface mit Scan(state ScanState, verb rune) error implementieren und werden dann von fmt.Scan direkt unterstützt — selten gebraucht, aber elegant.

io.EOF als legitimes Ende

Bei Pipe-Eingaben ist io.EOF kein Fehler im engeren Sinn, sondern Signal für „Stream zu Ende". Loops, die mit Scan lesen, müssen io.EOF als sauberen Abbruch behandeln.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht