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.

Go scanner_def.go
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.

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

Go scanstate_api.go
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.

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

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

Go fehler_demo.go
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)
}
Output
{0} Port: 99999 ausserhalb 1..65535

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

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

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

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht