Das Interface fmt.Formatter ist die mächtigste Hook im gesamten fmt-Paket. Während Stringer und GoStringer jeweils nur eine einzige feste Darstellung liefern, übernimmt ein Formatter das Rendering komplett selbst: er bekommt das aktuelle Verb als rune und einen fmt.State, der Breite, Präzision, Flags und den Ziel-Writer kapselt. Damit lässt sich pro Verb — %v, %s, %d, %x, %#v — eine andere Logik implementieren, ohne den Aufruf-Code in Printf ändern zu müssen. Library-Autoren von Decimal-, BigInt- oder Custom-Time-Typen kommen ohne diese Schnittstelle nicht aus.

Die Signatur ist kompakt, aber jeder Parameter trägt Gewicht. Format gibt nichts zurück — der gesamte Output landet direkt im übergebenen State, der selbst ein io.Writer ist. Das verb entspricht dem Buchstaben hinter dem Prozentzeichen im Format-String, also etwa 'v' bei %v oder 'x' bei %x.

Go formatter_iface.go
package fmt

// Formatter ist von Typen implementiert, die ihre eigene
// Formatierung übernehmen wollen.
type Formatter interface {
    Format(f State, verb rune)
}

Wer Formatter implementiert, gibt damit explizit das Versprechen ab, jedes denkbare Verb sinnvoll behandeln zu können — entweder mit einer eigenen Darstellung oder mit einem expliziten Fehlerfall. Das ist gleichzeitig die größte Verantwortung dieser Schnittstelle: vergisst die Implementation ein Verb, sieht der Aufrufer nichts, wo er etwas erwartet.

Der Parameter f State ist der zentrale Kontext-Container. Er ist gleichzeitig der Ziel-io.Writer — ein fmt.Fprintf(f, ...) oder f.Write(...) schreibt direkt in den Output-Stream des aufrufenden Printf. Zusätzlich liefert er drei Inspektions-Methoden: Width() und Precision() geben jeweils (int, bool) zurück, wobei das bool signalisiert, ob der Wert überhaupt gesetzt wurde, und Flag(c int) bool prüft einzelne Flag-Zeichen.

Go state_inspect.go
package main

import "fmt"

type Probe struct{}

func (Probe) Format(f fmt.State, verb rune) {
    w, hasW := f.Width()
    p, hasP := f.Precision()
    fmt.Fprintf(f, "verb=%c width=(%d,%v) precision=(%d,%v) plus=%v hash=%v",
        verb, w, hasW, p, hasP, f.Flag('+'), f.Flag('#'))
}

func main() {
    fmt.Printf("%10.3+v\n", Probe{})
    fmt.Printf("%#x\n", Probe{})
}
Output
verb=v width=(10,true) precision=(3,true) plus=true hash=false
verb=x width=(0,false) precision=(0,false) plus=false hash=true

Diese Inspektion ist der Schlüssel zu jedem ernsthaften Formatter: erst nach dem Auslesen des State weiß die Methode, wie viel Padding gewünscht ist, wie viele Nachkommastellen gerendert werden sollen und welche Flag-Variante des Verbs aktiv ist. Ohne diese Schritte verhält sich der Typ wie ein primitiver Stringer — die ganze Mehrkraft des Interfaces bleibt ungenutzt.

In der Praxis ist der Rumpf jeder Format-Methode ein switch über das verb. Dort werden die unterstützten Verben aufgezählt und jeweils unterschiedlich behandelt; ein default-Zweig sollte explizit signalisieren, dass das Verb nicht passt, üblicherweise mit einer %!Verb(typ=wert)-Fehlermeldung im Stil der fmt-internen Diagnostik.

Go verb_routing.go
package main

import "fmt"

type Tag string

func (t Tag) Format(f fmt.State, verb rune) {
    switch verb {
    case 'v', 's':
        fmt.Fprintf(f, "#%s", string(t))
    case 'q':
        fmt.Fprintf(f, "%q", "#"+string(t))
    default:
        fmt.Fprintf(f, "%%!%c(Tag=%s)", verb, string(t))
    }
}

func main() {
    t := Tag("go")
    fmt.Printf("%v\n", t)
    fmt.Printf("%q\n", t)
    fmt.Printf("%d\n", t)
}
Output
#go
"#go"
%!d(Tag=go)

Der default-Pfad ist kein Detail, sondern Pflicht: er macht für Aufrufer sichtbar, dass das Verb nicht unterstützt wird, statt stillschweigend leere Strings oder falsche Darstellungen zu liefern. Die Konvention %!verb(typ=wert) ist deshalb wertvoll, weil sie sich in das gewohnte fmt-Fehlerbild einfügt und beim Lesen sofort als Format-Fehler erkennbar ist.

Implementiert ein Typ gleichzeitig Formatter, GoStringer und Stringer, gewinnt immer der Formatter. Das fmt-Paket prüft die Interfaces in genau dieser Reihenfolge und ruft, sobald Format vorhanden ist, ausschließlich diese Methode auf — String() und GoString() werden für die fmt-Ausgabe nicht mehr konsultiert.

Go hook_order.go
package main

import "fmt"

type Triple struct{ N int }

func (t Triple) String() string                    { return "string-form" }
func (t Triple) GoString() string                  { return "go-string-form" }
func (t Triple) Format(f fmt.State, verb rune) {
    fmt.Fprintf(f, "formatter[%c]:%d", verb, t.N)
}

func main() {
    t := Triple{N: 7}
    fmt.Printf("%v\n", t)
    fmt.Printf("%s\n", t)
    fmt.Printf("%#v\n", t)
}
Output
formatter[v]:7
formatter[s]:7
formatter[#v]:7

Wer also Formatter einführt, übernimmt damit die Verantwortung, auch die Verben sinnvoll zu bedienen, die zuvor String() oder GoString() abgedeckt haben. Die zusätzlichen Methoden bleiben für direkte Aufrufer wie t.String() nützlich, sie spielen für Printf aber keine Rolle mehr.

Die Flags, die zwischen % und Verb stehen können, werden im Format-Code als einzelne rune-Werte abgefragt. Üblich sind fünf Stück, jeweils mit klassisch festgelegter Semantik aus der C-Tradition.

Go flag_table.go
// f.Flag('+') // Vorzeichen erzwingen (auch bei positiven Zahlen)
// f.Flag('-') // Links-Ausrichtung statt rechts (Padding rechts)
// f.Flag('#') // alternative Form: 0x-Prefix, Go-Syntax, ausführlich
// f.Flag(' ') // Leerzeichen vor positiven Zahlen
// f.Flag('0') // Mit Nullen padden statt mit Leerzeichen

Diese Tabelle ist keine harte Regel des fmt-Pakets, sondern eine starke Konvention: kein Aufrufer erwartet, dass %#x plötzlich ein Komma einfügt oder dass %+d das Vorzeichen verschluckt. Wer in der eigenen Format-Methode mit Flags arbeitet, sollte sich an dieser Semantik orientieren, damit der Typ sich für andere Entwickler erwartbar verhält.

Ein typischer Anwendungsfall ist ein Festkomma-Typ, der je nach Verb anders gerendert wird. Bei %v soll die Standard-Darstellung kommen, bei %f eine echte Fließkomma-Sicht inklusive Precision, und Width sorgt zusätzlich für rechtsbündiges Padding mit Leerzeichen oder Nullen.

Go decimal_formatter.go
package main

import (
    "fmt"
    "strings"
)

// Decimal speichert einen Festkomma-Wert: Value/10^Scale.
type Decimal struct {
    Value int64
    Scale int
}

func (d Decimal) Format(f fmt.State, verb rune) {
    var body string
    switch verb {
    case 'v', 's':
        body = d.render(d.Scale)
    case 'f':
        prec, ok := f.Precision()
        if !ok {
            prec = d.Scale
        }
        body = d.render(prec)
    default:
        fmt.Fprintf(f, "%%!%c(Decimal=%d/10^%d)", verb, d.Value, d.Scale)
        return
    }

    if w, ok := f.Width(); ok && w > len(body) {
        pad := strings.Repeat(" ", w-len(body))
        if f.Flag('0') {
            pad = strings.Repeat("0", w-len(body))
        }
        if f.Flag('-') {
            body = body + pad
        } else {
            body = pad + body
        }
    }
    fmt.Fprint(f, body)
}

func (d Decimal) render(prec int) string {
    if prec <= 0 {
        return fmt.Sprintf("%d", d.Value)
    }
    s := fmt.Sprintf("%0*d", prec+1, d.Value)
    return s[:len(s)-prec] + "." + s[len(s)-prec:]
}

func main() {
    d := Decimal{Value: 12345, Scale: 2} // 123.45
    fmt.Printf("[%v]\n", d)
    fmt.Printf("[%.4f]\n", d)
    fmt.Printf("[%10.2f]\n", d)
    fmt.Printf("[%-10.2f]\n", d)
    fmt.Printf("[%010.2f]\n", d)
}
Output
[123.45]
[1.2345]
[    123.45]
[123.45    ]
[0000123.45]

Der Decimal-Typ zeigt das volle Repertoire: Precision steuert die Skalierung, Width das Padding, das '0'-Flag wechselt zwischen Leerzeichen und Nullen, und '-' kippt die Ausrichtung. Genau diese Kombination ist mit Stringer nicht erreichbar — eine einzelne String()-Methode kennt weder Width noch Precision und kann deshalb keine kontextabhängige Darstellung liefern.

Das zweite Beispiel ist ein Byte-Slice-Wrapper, der vier verschiedene Verben sinnvoll bedient: %x rendert lowercase ohne Trennung, %X uppercase, %#x ergänzt das 0x-Prefix, und %v liefert die Go-Slice-Literal-Schreibweise. Alles in einer einzigen Methode, sauber durch Verb und Flag verzweigt.

Go hex_bytes.go
package main

import (
    "encoding/hex"
    "fmt"
    "strings"
)

type HexBytes []byte

func (b HexBytes) Format(f fmt.State, verb rune) {
    switch verb {
    case 'x':
        s := hex.EncodeToString(b)
        if f.Flag('#') {
            fmt.Fprint(f, "0x"+s)
            return
        }
        fmt.Fprint(f, s)
    case 'X':
        s := strings.ToUpper(hex.EncodeToString(b))
        if f.Flag('#') {
            fmt.Fprint(f, "0X"+s)
            return
        }
        fmt.Fprint(f, s)
    case 'v', 's':
        parts := make([]string, len(b))
        for i, x := range b {
            parts[i] = fmt.Sprintf("0x%02x", x)
        }
        fmt.Fprint(f, "["+strings.Join(parts, ", ")+"]")
    default:
        fmt.Fprintf(f, "%%!%c(HexBytes=%d)", verb, len(b))
    }
}

func main() {
    b := HexBytes{0xCA, 0xFE, 0xBA, 0xBE}
    fmt.Printf("%x\n", b)
    fmt.Printf("%X\n", b)
    fmt.Printf("%#x\n", b)
    fmt.Printf("%v\n", b)
    fmt.Printf("%d\n", b)
}
Output
cafebabe
CAFEBABE
0xcafebabe
[0xca, 0xfe, 0xba, 0xbe]
%!d(HexBytes=4)

Dieser Aufbau zeigt, warum Formatter für Library-Code so wertvoll ist: ein einziger Typ deckt alle gängigen Hex-Schreibweisen ab, ohne dass Aufrufer separate Methoden wie ToLowerHex() oder ToUpperHex() lernen müssen. Die Verben übernehmen die Rolle eines kleinen DSL — und der Typ verhält sich nach außen exakt wie eingebaute primitive Typen.

Formatter schlägt Stringer und GoStringer

Implementiert ein Typ Format, ruft fmt für %v, %s und %#v ausschließlich diese Methode — String() und GoString() werden für die fmt-Ausgabe komplett übergangen.

State ist gleichzeitig io.Writer

fmt.State erfüllt io.Writer, deshalb funktioniert fmt.Fprintf(f, ...) oder f.Write(buf) direkt — kein separater Output-Parameter, kein Rückgabewert nötig.

Width und Precision sind optional

f.Width() und f.Precision() liefern (int, bool); das zweite Element zeigt, ob der Wert im Format-String überhaupt angegeben wurde. Defaultwerte gehören in den Formatter selbst.

Flag-Inspektion via Flag(rune)

f.Flag('+'), f.Flag('#'), f.Flag('0'), f.Flag('-'), f.Flag(' ') fragen die fünf gängigen Flags ab — ohne diese Methode bleibt der Formatter blind für die Aufrufer-Intention.

Fallback für unbekannte Verben

Ein default-Zweig im switch verb sollte explizit %!verb(typ=wert) im fmt-Stil schreiben, damit Format-Fehler sichtbar werden statt still ins Leere zu laufen.

Pointer- vs. Value-Receiver

Wird Format auf *T definiert, greift sie nur für Pointer-Werte; bei großen Structs oder Mutations-Bedarf ist der Pointer-Receiver Pflicht, bei kleinen Werttypen genügt Value.

Library-typisch: math/big, time, decimal

math/big.Int, math/big.Float und gängige Decimal-Pakete implementieren Formatter, weil sie ohne Verb-Routing keine sinnvolle Darstellung für %d, %x, %e, %g zugleich liefern könnten.

Nicht für Trivialfälle

Wenn ein Typ ohnehin nur eine einzige Darstellung kennt, ist Stringer die richtige Wahl — Formatter lohnt sich erst, wenn Verb, Width, Precision oder Flags echtes Verhalten steuern sollen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht