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.
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.
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{})
}verb=v width=(10,true) precision=(3,true) plus=true hash=false
verb=x width=(0,false) precision=(0,false) plus=false hash=trueDiese 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.
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)
}#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.
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)
}formatter[v]:7
formatter[s]:7
formatter[#v]:7Wer 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.
// 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 LeerzeichenDiese 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.
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)
}[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.
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)
}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.