Das Interface fmt.Scanner ist das Spiegelbild zu fmt.Formatter: Während Formatter die Ausgabe-Richtung steuert, übernimmt Scanner die Eingabe-Richtung. Implementiert ein Typ die Methode Scan(state ScanState, verb rune) error, ruft die Scan-Familie (fmt.Scan, fmt.Sscan, fmt.Fscan samt Verwandten) diese Methode automatisch auf, sobald ein Argument vom passenden Pointer-Typ übergeben wird. So lassen sich domänenspezifische Eingabeformate — Versionen, Farben, Koordinaten, Datumsformate — direkt in die idiomatische Scan-API einhängen, ohne den Aufrufer mit Parsing-Logik zu belasten.
Eine harte Regel zieht sich durch das gesamte Interface: Scan muss auf einem Pointer-Receiver definiert sein. Die Methode schreibt in den Receiver hinein, und ein Value-Receiver würde nur an einer Kopie arbeiten, die nach dem Return verworfen wird. Das ist kein Stilthema, sondern Voraussetzung für Funktionsfähigkeit.
Die Definition ist minimal — eine einzige Methode, zwei Parameter, ein Fehler-Return. Hinter dieser schlanken Signatur steckt aber die volle Mächtigkeit des ScanState-Interfaces, das innerhalb der Methode zur Verfügung steht und alle nötigen Low-Level-Operationen für den Input-Stream bündelt.
package fmt
// Scanner wird von jedem Wert implementiert, der eine
// Scan-Methode hat, die seinen Inhalt aus dem ScanState liest.
type Scanner interface {
Scan(state ScanState, verb rune) error
}Der Parameter state liefert die Tokenisierungs- und Lese-Primitive, verb enthält das einzelne Verb-Zeichen aus dem Format-String (bei fmt.Scanf("%v", &t) also 'v'). Der Rückgabewert ist ein klassischer error — entweder nil bei Erfolg, oder ein konkreter Fehler, der die Scan-Operation insgesamt abbricht.
Weil die Scan-Methode in den Receiver hineinschreibt, muss sie zwingend auf einem Pointer-Receiver definiert werden. Ein Value-Receiver erhält eine Kopie und wäre wirkungslos — der Aufrufer würde nach fmt.Sscan(s, &t) einen unveränderten t vorfinden. Die Scan-Familie prüft das Interface gegen den Pointer-Typ des übergebenen Arguments, weshalb &t ohnehin Pflicht ist.
package main
import (
"fmt"
)
type Coord struct {
X, Y int
}
// RICHTIG: Pointer-Receiver, schreibt in den Original-Receiver
func (c *Coord) Scan(state fmt.ScanState, verb rune) error {
_, err := fmt.Fscanf(state, "(%d,%d)", &c.X, &c.Y)
return err
}
func main() {
var c Coord
_, err := fmt.Sscan("(3,7)", &c)
fmt.Println(c, err)
}{3 7} <nil>Wäre die Methode mit func (c Coord) Scan(...) definiert, würde Go den Pointer *Coord zwar formal als Scanner akzeptieren (Method-Set-Regel), aber die Zuweisungen in c.X und c.Y landeten in einer kurzlebigen Kopie. Faustregel: Scan-Methoden bekommen immer einen Pointer-Receiver — ohne Ausnahme.
Innerhalb der Scan-Methode wird der gesamte Input über das ScanState-Interface konsumiert. Die wichtigsten Methoden sind Token (tokenbasiertes Lesen mit optionalem Filter), Read (rohes Lesen für io.Reader-Kompatibilität), Width (das aus dem Verb extrahierte Breiten-Limit, z. B. %5v), SkipSpace (Whitespace explizit überspringen) und UnreadRune (einen Rune zurückstellen).
type ScanState interface {
ReadRune() (r rune, size int, err error)
UnreadRune() error
SkipSpace()
Token(skipSpace bool, f func(rune) bool) (token []byte, err error)
Width() (wid int, ok bool)
Read(buf []byte) (n int, err error)
}Die zentrale Methode ist Token. Sie liest ab der aktuellen Position so lange Runes, wie die Filter-Funktion f true zurückgibt, und liefert das Ergebnis als Byte-Slice. Mit skipSpace=true wird vorlaufender Whitespace verworfen — exakt das Verhalten, das fmt.Scan standardmäßig zwischen Argumenten zeigt.
package main
import (
"fmt"
"unicode"
)
type Word struct{ Value string }
func (w *Word) Scan(state fmt.ScanState, verb rune) error {
tok, err := state.Token(true, func(r rune) bool {
return unicode.IsLetter(r)
})
if err != nil {
return err
}
w.Value = string(tok)
return nil
}
func main() {
var w Word
fmt.Sscan(" Hallo123", &w)
fmt.Printf("%q\n", w.Value)
}"Hallo"Token stoppt beim ersten Zeichen, das die Filter-Funktion ablehnt (1 ist kein Buchstabe), ohne dieses Zeichen zu konsumieren — der nächste Scan-Aufruf sieht es also wieder. Das macht die API kompositionsfreundlich: mehrere Scanner können hintereinander am selben Stream arbeiten.
Der verb-Parameter erlaubt es, je nach Format-Verb unterschiedliches Parse-Verhalten zu implementieren. Praktisch relevant sind 'v' (default), 's' (string-artige Eingabe) und gelegentlich 'q' (quoted). Bei unbekanntem Verb sollte ein aussagekräftiger Fehler zurückgegeben werden, damit der Aufrufer die Fehlanwendung sofort erkennt.
package main
import (
"errors"
"fmt"
)
type Tag struct{ Name string }
func (t *Tag) Scan(state fmt.ScanState, verb rune) error {
switch verb {
case 'v', 's':
tok, err := state.Token(true, func(r rune) bool {
return r != ' ' && r != '\n' && r != '\t'
})
if err != nil {
return err
}
t.Name = string(tok)
return nil
default:
return errors.New("Tag: unbekanntes Verb %" + string(verb))
}
}
func main() {
var t Tag
_, err := fmt.Sscanf("release-v2", "%s", &t)
fmt.Println(t, err)
}{release-v2} <nil>Das Switch-Pattern hält die Methode lesbar und macht erweiterbares Verhalten explizit. Wer nur ein Verb unterstützt, sollte den Default-Fall trotzdem nicht weglassen — sonst schluckt die Methode falsch genutzte Aufrufe stillschweigend.
Jeder von ScanState zurückgegebene Fehler — typisch io.EOF oder io.ErrUnexpectedEOF — sollte sauber durchgereicht werden. Die Scan-Familie wertet io.EOF korrekt aus (sie behandelt es als „nichts mehr da, aber kein Defekt"), während andere Fehler die gesamte Scan-Operation abbrechen. Wichtig: partielle Reads dürfen den Receiver nicht in einem halb-initialisierten Zustand hinterlassen, wenn die Methode mit Fehler zurückkehrt.
package main
import (
"errors"
"fmt"
"unicode"
)
type Port struct{ N int }
func (p *Port) Scan(state fmt.ScanState, verb rune) error {
tok, err := state.Token(true, unicode.IsDigit)
if err != nil {
return err
}
if len(tok) == 0 {
return errors.New("Port: keine Ziffern gefunden")
}
var n int
for _, b := range tok {
n = n*10 + int(b-'0')
}
if n < 1 || n > 65535 {
return fmt.Errorf("Port: %d ausserhalb 1..65535", n)
}
p.N = n // erst NACH Validierung zuweisen
return nil
}
func main() {
var p Port
_, err := fmt.Sscan("99999", &p)
fmt.Println(p, err)
}{0} Port: 99999 ausserhalb 1..65535Die Reihenfolge ist entscheidend: erst vollständig parsen und validieren, dann dem Receiver zuweisen. So bleibt p.N bei 0, wenn die Validierung scheitert — der Aufrufer sieht keine korrupten Teilwerte.
Ein häufiges Praxis-Szenario: SemVer-artige Versionsstrings sollen direkt aus einem Konfig-Stream oder einer CLI-Eingabe in einen typisierten Version-Wert wandern. Mit fmt.Fscanf innerhalb der Scan-Methode lässt sich das in wenigen Zeilen formulieren — die Methode nutzt selbst wieder die Scan-Familie, weil der ScanState gleichzeitig ein io.Reader ist.
package main
import (
"fmt"
)
type Version struct {
Major, Minor, Patch int
}
func (v *Version) Scan(state fmt.ScanState, verb rune) error {
var maj, min, pat int
_, err := fmt.Fscanf(state, "%d.%d.%d", &maj, &min, &pat)
if err != nil {
return fmt.Errorf("Version: %w", err)
}
v.Major, v.Minor, v.Patch = maj, min, pat
return nil
}
func (v Version) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}
func main() {
var v Version
_, err := fmt.Sscan("1.24.3", &v)
fmt.Println(v, err)
}1.24.3 <nil>Das Muster ist elegant: fmt.Fscanf(state, ...) delegiert die Tokenisierung an die Standard-Implementierung, das Format-String %d.%d.%d erzwingt die exakte Punkt-Trennung. Schlägt die Eingabe fehl (z. B. 1.24-rc1), wird der errors.Wrap-artige Kontext Version: ... mitgeliefert.
Der zweite Fall zeigt die Filter-Funktion von Token in voller Stärke: ein Hex-Farbcode beginnt mit #, gefolgt von genau sechs hexadezimalen Ziffern. Die Scan-Methode konsumiert zunächst das # über ReadRune, dann den Hex-Body via Token mit einem Filter, der nur Hex-Zeichen zulässt.
package main
import (
"errors"
"fmt"
"strconv"
)
type HexColor struct{ R, G, B uint8 }
func isHex(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func (h *HexColor) Scan(state fmt.ScanState, verb rune) error {
state.SkipSpace()
r, _, err := state.ReadRune()
if err != nil {
return err
}
if r != '#' {
return errors.New("HexColor: erwarte '#' als Praefix")
}
tok, err := state.Token(false, isHex)
if err != nil {
return err
}
if len(tok) != 6 {
return fmt.Errorf("HexColor: 6 Hex-Zeichen erwartet, %d erhalten", len(tok))
}
n, err := strconv.ParseUint(string(tok), 16, 32)
if err != nil {
return fmt.Errorf("HexColor: %w", err)
}
h.R = uint8(n >> 16)
h.G = uint8(n >> 8)
h.B = uint8(n)
return nil
}
func main() {
var c HexColor
_, err := fmt.Sscan(" #ff8800", &c)
fmt.Printf("R=%d G=%d B=%d err=%v\n", c.R, c.G, c.B, err)
}R=255 G=136 B=0 err=<nil>SkipSpace macht den vorlaufenden Whitespace explizit weg, danach prüft ReadRune das #. Wichtig ist Token(false, ...): das skipSpace=false verhindert, dass zwischen # und Hex-Body Whitespace toleriert wird — # ff8800 würde so korrekt als Fehler markiert. Die finale Konvertierung lehnt sich an strconv.ParseUint an, weil das Bit-Shifting hand-codiert kein Mehrwert wäre und der Standard-Parser den Hex-Overflow zuverlässig abfängt.
Pointer-Receiver-Pflicht
Scan schreibt in den Receiver — ein Value-Receiver würde nur eine Kopie verändern. Immer func (p *T) Scan(...), sonst ist die Methode wirkungslos.
Spiegel zu Formatter
Scanner ist die Lese-Seite, Formatter die Schreib-Seite. Beide Interfaces erlauben es eigenen Typen, sich nahtlos in die fmt-Familie einzuklinken.
Token mit Filter-Funktion
state.Token(skipSpace, f) ist das Arbeitspferd — f entscheidet runen-weise, was zum Token gehoert. Stoppt beim ersten Reject ohne Konsum.
SkipSpace explizit
state.SkipSpace() ueberspringt Whitespace bewusst — sinnvoll vor ReadRune oder wenn Token mit skipSpace=false aufgerufen wird.
Width-Limit aus Verb
state.Width() liefert die im Verb angegebene Breite (z. B. %5v) als (int, bool). Praktisch fuer fixed-width-Eingaben mit harten Grenzen.
EOF-Handling
io.EOF darf weitergereicht werden — die Scan-Familie wertet das korrekt aus. Andere Fehler brechen die gesamte Scan-Operation ab.
Partielle Reads sauber abfangen
Receiver erst NACH erfolgreicher Validierung zuweisen — sonst hinterlaesst ein Fehler-Return halb-initialisierte Werte beim Aufrufer.
Selten in idiomatic Go
In der Praxis sind bufio.Scanner + strconv oder ein dedizierter Parse-Konstruktor haeufiger. fmt.Scanner glaenzt, wenn die Scan-Familie ohnehin im Spiel ist.