fmt.State ist der Laufzeit-Kontext, den das fmt-Paket einer eigenen Format-Methode überreicht, sobald diese durch das Formatter-Interface aufgerufen wird. Das Interface vereint zwei Rollen in einem einzigen Objekt: Es ist Schreibziel (durch embedded io.Writer) und Inspektionspunkt für die Modifier, die im Format-String hinter dem Prozent-Zeichen standen — also Width, Precision und die Flags +, -, #, 0, Leerzeichen.
Ohne State wäre Formatter.Format ein blinder Aufruf: Man bekäme das Verb, wüsste aber nicht, wohin geschrieben werden soll und welche Feinjustierung der Nutzer im Format-String wirklich verlangt hat. Die Trennung in vier sehr kleine Methoden — Write, Width, Precision, Flag — sorgt dafür, dass jede Format-Implementierung genau die Information abruft, die sie tatsächlich braucht, und den Rest dem Default überlässt.
Interface-Methoden
State ist bewusst minimal gehalten. Die vier Methoden decken jeden Aspekt eines Format-Verbs ab, der zur Laufzeit relevant ist — und nichts darüber hinaus.
type State interface {
// Write ist die Output-Senke — State ist ein io.Writer.
Write(b []byte) (n int, err error)
// Width liefert die Breite aus dem Format-String.
// ok == false, wenn keine Breite angegeben wurde.
Width() (wid int, ok bool)
// Precision liefert die Präzision aus dem Format-String.
// ok == false, wenn keine Präzision angegeben wurde.
Precision() (prec int, ok bool)
// Flag meldet, ob die Flag-Rune c im Format-String stand.
Flag(c int) bool
}Die drei Getter geben keine nackten int-Werte zurück, sondern jeweils ein (int, bool)-Paar. Damit lässt sich sauber unterscheiden, ob der Aufrufer %5d (Breite gesetzt) oder %d (keine Breite) geschrieben hat — beides würde bei einer reinen int-Rückgabe sonst dieselbe Null produzieren und die Default-Logik aushebeln.
Write — die Output-Senke
State ist selbst ein io.Writer. Innerhalb von Format schreibt man also direkt auf den Parameter — es gibt keinen separaten Buffer, keinen Return-String, kein io.WriteString über Umwege. Jedes Byte, das durch die Format-Methode rausgeht, landet exakt dort, wo fmt.Printf, fmt.Sprintf oder fmt.Fprintf es haben wollen.
func (t MyType) Format(s fmt.State, verb rune) {
// direktes Schreiben in den State
s.Write([]byte("hallo "))
// bequemer mit fmt.Fprintf — s ist ein io.Writer
fmt.Fprintf(s, "welt (%c)", verb)
}In der Praxis ruft man selten Write direkt mit Byte-Slice auf. Sehr viel häufiger reicht man den State einfach an fmt.Fprintf oder fmt.Fprint weiter — beides funktioniert, weil das Interface die io.Writer-Signatur ohnehin garantiert.
Width und Precision — Verb-Modifier inspizieren
Width() liefert die Zahl direkt nach dem Prozent (%10d → wid=10), Precision() die Zahl nach dem Punkt (%.3f → prec=3). Beide melden über das ok-Flag, ob der Wert überhaupt im Format-String stand. Genau diese Unterscheidung ermöglicht es, einen Default zu setzen, der nur greift, wenn der Nutzer nichts vorgegeben hat.
func (t MyType) Format(s fmt.State, verb rune) {
width, hasWidth := s.Width()
prec, hasPrec := s.Precision()
if !hasWidth {
width = 8 // sinnvoller Default
}
if !hasPrec {
prec = 2
}
fmt.Fprintf(s, "%*.*f", width, prec, float64(t))
}Der Trick mit %*.*f ist hier kein Zufall: Das Sternchen lässt fmt die Breite bzw. Präzision aus einem zusätzlichen Argument lesen — so reicht man die vom State gemeldeten Werte sauber durch und überlässt das eigentliche Padding der Standard-Implementierung.
Flag(c int) bool — die fünf Format-Flags
Flag nimmt eine Rune (in Form eines int-Codepoints) und antwortet mit true, wenn dieses Flag im Format-String stand. Insgesamt gibt es fünf Flags, die fmt standardmäßig parst.
func (t MyType) Format(s fmt.State, verb rune) {
plus := s.Flag('+') // %+v Vorzeichen erzwingen / Feldnamen
minus := s.Flag('-') // %-10v linksbündig pollstern
hash := s.Flag('#') // %#v alternative Darstellung (z. B. 0x)
zero := s.Flag('0') // %05d mit Nullen auffüllen
space := s.Flag(' ') // % d Leerzeichen vor positiven Zahlen
fmt.Fprintf(s, "+:%v -:%v #:%v 0:%v ' ':%v",
plus, minus, hash, zero, space)
}Die Rune-Notation '+' ist semantisch dasselbe wie der Codepoint 43. Innerhalb von Format-Methoden ist die Rune-Form deutlich lesbarer und sollte immer bevorzugt werden — der int-Parameter ist nur ein historisches Detail der Interface-Definition.
Typische Verwendung in Formatter.Format
Eine sauber strukturierte Format-Methode geht in drei Schritten vor: zuerst Verb prüfen, dann State inspizieren, schließlich passendes Layout schreiben. Genau diese Reihenfolge spiegelt wider, wie fmt intern auch arbeitet — und macht den Code lesbar für alle, die das Muster aus der Standard-Library kennen.
func (t MyType) Format(s fmt.State, verb rune) {
// 1) Verb-Dispatch
switch verb {
case 'v', 's':
// 2) Modifier inspizieren
width, hasWidth := s.Width()
showSign := s.Flag('+')
// 3) Layout schreiben
if showSign {
s.Write([]byte("+"))
}
if hasWidth {
fmt.Fprintf(s, "%-*s", width, t.label())
} else {
fmt.Fprint(s, t.label())
}
default:
fmt.Fprintf(s, "%%!%c(MyType=%s)", verb, t.label())
}
}Der default-Zweig folgt einer Konvention, die fmt selbst nutzt: Bei einem unbekannten Verb wird %!v(TYP=wert) geschrieben — so erkennt der Nutzer sofort, dass sein Format-String nicht zum Typ passt, ohne dass das Programm crasht.
Praxis 1 — Custom-Decimal mit Width-Padding
Ein klassischer Anwendungsfall: ein Typ, der wie eine Zahl gerendert wird, aber Breite und Ausrichtung (- für linksbündig) selbst respektieren soll. Damit verhält sich der eigene Typ in Spalten-Layouts wie ein nativer numerischer Wert.
package main
import (
"fmt"
"strings"
)
type Cents int64
func (c Cents) Format(s fmt.State, verb rune) {
text := fmt.Sprintf("%d.%02d €", int64(c)/100, int64(c)%100)
width, hasWidth := s.Width()
if !hasWidth || width <= len(text) {
s.Write([]byte(text))
return
}
pad := strings.Repeat(" ", width-len(text))
if s.Flag('-') {
s.Write([]byte(text + pad)) // linksbündig
} else {
s.Write([]byte(pad + text)) // rechtsbündig
}
}
func main() {
c := Cents(12345)
fmt.Printf("[%v]\n", c)
fmt.Printf("[%10v]\n", c)
fmt.Printf("[%-10v]\n", c)
}[123.45 €]
[ 123.45 €]
[123.45 € ]Die Logik ist bewusst kompakt: Erst wird der eigentliche Text gebaut, dann entscheidet das Vorhandensein einer Width über das Padding und das --Flag über die Richtung. Diese drei Zeilen ersetzen, was sonst in einer separaten String-Bibliothek versteckt wäre — und integrieren sich nahtlos in jeden fmt.Printf-Aufruf.
Praxis 2 — Bytes-Typ mit Hex-Flag-Logik
Das #-Flag schaltet bei vielen Standard-Verben in eine alternative Darstellung — bei %x heißt das den 0x-Prefix mitschreiben, bei %o ein führendes 0. Diese Konvention kann ein eigener Typ ohne Mehraufwand übernehmen, damit Nutzer ihn so behandeln können, wie sie es von int her gewohnt sind.
package main
import "fmt"
type Bytes []byte
func (b Bytes) Format(s fmt.State, verb rune) {
switch verb {
case 'x':
if s.Flag('#') {
s.Write([]byte("0x"))
}
for _, c := range b {
fmt.Fprintf(s, "%02x", c)
}
case 'v':
fmt.Fprintf(s, "Bytes(len=%d)", len(b))
default:
fmt.Fprintf(s, "%%!%c(Bytes)", verb)
}
}
func main() {
b := Bytes{0xDE, 0xAD, 0xBE, 0xEF}
fmt.Printf("%x\n", b)
fmt.Printf("%#x\n", b)
fmt.Printf("%v\n", b)
}deadbeef
0xdeadbeef
Bytes(len=4)Die Verzweigung über s.Flag('#') ist hier das Herzstück: Ein und dasselbe Verb verhält sich je nach Flag unterschiedlich, ohne dass der Aufrufer eine separate Methode oder einen anderen Verb-Buchstaben kennen muss. Genau dieser Mechanismus macht State zum Bindeglied zwischen Format-String-Grammatik und individueller Typ-Logik.
Embedded io.Writer
State erbt Write([]byte) (int, error) — jeder fmt.Fprintf-kompatible Code funktioniert direkt mit dem State-Parameter.
Width / Precision liefern (int, bool)
Das ok-Flag unterscheidet sauber zwischen „nicht gesetzt" und „explizit 0" — Default-Werte greifen nur bei ok == false.
Flag-Konstanten als Rune
Flag('+'), Flag('-'), Flag('#'), Flag('0'), Flag(' ') decken alle fünf Standard-Flags ab — Rune-Literale sind lesbarer als Codepoint-Zahlen.
fmt.Fprintf(state, ...) als Helfer
Statt manuell Bytes zu bauen, einfach den State an fmt.Fprintf weitergeben — spart Boilerplate und nutzt die volle Standard-Formatierung.
Kein direkter Verb-Zugriff über State
Das Verb kommt als separater rune-Parameter in Format — State selbst kennt das Verb nicht und bietet keine Methode dafür.
Custom-Verben sind möglich
Über Formatter.Format darf ein Typ beliebige Verben akzeptieren — Konvention ist ein Fehler-Fallback wie %!v(TYP=wert) für unbekannte.
State ist read-only für Modifier
Width, Precision und Flags lassen sich abfragen, aber nicht ändern — der Format-String bestimmt sie, die Implementierung folgt.
State lebt nur während Format
Den State-Pointer nicht für späteren Gebrauch wegspeichern — er ist nur innerhalb des laufenden Format-Aufrufs gültig.