Format-Verben sind das Herz aller Printf-Familien in Go: Sie entscheiden, wie ein Wert in seine textuelle Darstellung übersetzt wird. Ein einzelnes Zeichen hinter dem Prozentzeichen — d, s, v, q, x — steuert, ob aus der Zahl 255 die Ausgabe 255, ff, 377 oder das Zeichen ÿ wird.
Die Verben gliedern sich in zwei große Familien. Generelle Verben (%v, %+v, %#v, %T, %%) funktionieren mit jedem Typ und fragen über Reflection nach einer sinnvollen Darstellung. Typisierte Verben sind an eine Typklasse gebunden — %d verlangt eine Ganzzahl, %f einen Float, %s einen String oder ein []byte. Werden sie falsch eingesetzt, schreibt fmt eine %!verb(typ=wert)-Fehlermarkierung in die Ausgabe statt eines harten Compile-Fehlers.
Diese Seite ist die vollständige Referenz. Verb-Modifikatoren wie Breite, Präzision und Flags (%5.2f, %-10s, %+d) sind ein eigenes Thema und werden auf der Seite Width, Precision, Flags behandelt — hier konzentrieren wir uns auf das Verb selbst.
Generelle Verben — die Allrounder
Generelle Verben akzeptieren jeden Typ. Sie sind der Einstiegspunkt für Debug-Output, Logging und alles, wo der konkrete Typ entweder bekannt oder uninteressant ist. Intern greift fmt auf Reflection zurück und prüft zusätzlich, ob der Wert ein Stringer-, GoStringer- oder Formatter-Interface implementiert.
| Verb | Bedeutung |
|---|---|
%v | Default-Format — kompakteste sinnvolle Darstellung |
%+v | Wie %v, aber bei Structs mit Feldnamen |
%#v | Go-Syntax-Repräsentation (so wie im Quelltext) |
%T | Typname des Werts (z. B. main.User, *int, []byte) |
%% | Literales Prozentzeichen — keinen Wert konsumieren |
Der praktische Unterschied wird erst am Beispiel sichtbar. Vor allem %+v ist beim Debuggen unbezahlbar: Aus {Anna 30} wird {Name:Anna Age:30}, und plötzlich ist klar, welche Zahl was bedeutet.
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Anna", Age: 30}
fmt.Printf("%%v = %v\n", u)
fmt.Printf("%%+v = %+v\n", u)
fmt.Printf("%%#v = %#v\n", u)
fmt.Printf("%%T = %T\n", u)
fmt.Printf("100%% Go\n")
}%v = {Anna 30}
%+v = {Name:Anna Age:30}
%#v = main.User{Name:"Anna", Age:30}
%T = main.User
100% Go%#v ist besonders nützlich, wenn man Test-Fixtures aus Laufzeitdaten generieren möchte: Die Ausgabe ist gültiger Go-Code und lässt sich direkt zurück in den Quelltext kopieren. %T hingegen ist das wichtigste Werkzeug, um bei interface{}-Werten herauszufinden, welcher konkrete Typ gerade vorliegt.
Integer-Verben — Zahlen in jeder Basis
Ganzzahlen lassen sich in verschiedenen Stellenwertsystemen darstellen, und Go bietet für jede gängige Basis ein eigenes Verb. Außerdem kann derselbe Integer-Wert als Zeichen (Rune) oder als Unicode-Codepoint interpretiert werden — derselbe int32 ist aus Sicht von fmt einfach eine Zahl mit verschiedenen Lesarten.
| Verb | Bedeutung |
|---|---|
%d | Dezimal (Basis 10) — der Default für Integer |
%b | Binär (Basis 2) |
%o | Oktal (Basis 8) |
%O | Oktal mit Präfix 0o |
%x | Hexadezimal, Kleinbuchstaben (ff) |
%X | Hexadezimal, Großbuchstaben (FF) |
%c | Zeichen, das dem Unicode-Codepoint entspricht |
%U | Unicode-Notation U+XXXX |
Wer %#x schreibt, bekommt das Präfix 0x mit dazu — das ist allerdings bereits ein Flag-Thema. Das Verb selbst liefert nur die nackten Ziffern.
package main
import "fmt"
func main() {
n := 255
fmt.Printf("%%d = %d\n", n)
fmt.Printf("%%b = %b\n", n)
fmt.Printf("%%o = %o\n", n)
fmt.Printf("%%O = %O\n", n)
fmt.Printf("%%x = %x\n", n)
fmt.Printf("%%X = %X\n", n)
r := 'ä'
fmt.Printf("%%c = %c\n", r)
fmt.Printf("%%U = %U\n", r)
}%d = 255
%b = 11111111
%o = 377
%O = 0o377
%x = ff
%X = FF
%c = ä
%U = U+00E4%c ist die Brücke zwischen Zahl und Schrift: Jeder rune- oder int-Wert wird als das UTF-8-kodierte Zeichen ausgegeben, das er beschreibt. %U liefert die kanonische Unicode-Notation, wie sie auch in offiziellen Code-Charts der Unicode-Konsortien steht — praktisch für Doku, Bug-Reports und Linter-Meldungen.
Float-Verben — Dezimal, wissenschaftlich, kompakt
Bei Fließkommazahlen kollidieren zwei Anforderungen: Lesbarkeit (Dezimaldarstellung) und Präzision/Skalierung (wissenschaftliche Notation). Go bietet beides — plus eine intelligente Variante, die selbst entscheidet, welche Form günstiger ist.
| Verb | Bedeutung |
|---|---|
%f | Dezimal ohne Exponent (3.141593) |
%F | Synonym für %f |
%e | Wissenschaftlich, kleiner Exponent (3.141593e+00) |
%E | Wissenschaftlich, großer Exponent (3.141593E+00) |
%g | %e für große/kleine Exponenten, sonst %f — minimal |
%G | Wie %g, aber mit großem E |
%b | Dezimal-loses binäres Exponentialformat (Debug-Format) |
%x | Hexadezimaler Float (0x1.921fb54442d18p+01) |
%X | Hexadezimaler Float, Großbuchstaben |
Die Defaultpräzision ist die kleinste Anzahl Stellen, die nötig ist, um den Wert eindeutig zu rekonstruieren. Bei %f werden ohne explizite Präzision sechs Nachkommastellen ausgegeben — das ist ein altes C-Erbe und überrascht regelmäßig.
package main
import (
"fmt"
"math"
)
func main() {
pi := math.Pi
fmt.Printf("%%f = %f\n", pi)
fmt.Printf("%%e = %e\n", pi)
fmt.Printf("%%E = %E\n", pi)
fmt.Printf("%%g = %g\n", pi)
fmt.Printf("%%G = %G\n", pi)
fmt.Printf("%%b = %b\n", pi)
fmt.Printf("%%x = %x\n", pi)
}%f = 3.141593
%e = 3.141593e+00
%E = 3.141593E+00
%g = 3.1415926535897931
%G = 3.1415926535897931
%b = 7074237752028440p-51
%x = 0x1.921fb54442d18p+01Die Faustregel: %f für Berichte und UI, %e für sehr große oder sehr kleine Zahlen, %g wenn das Programm selbst entscheiden soll. %x als Float-Verb ist ein Spezialfall — die hexadezimale Float-Darstellung garantiert verlustfreies Round-Tripping zwischen Text und IEEE-754 und ist deshalb das Format der Wahl, wenn Floats serialisiert und exakt wiederhergestellt werden müssen.
String- und Byte-Slice-Verben
Strings und []byte werden in fmt weitgehend gleich behandelt — ein []byte ist aus Verb-Sicht einfach ein String, dessen einzelne Bytes auch als Hex ausgegeben werden können. Das wichtige Verb hier ist %q: Es liefert einen Go-syntax-quoted String mit allen Steuerzeichen escapt — perfekt für Logs und Fehlermeldungen.
| Verb | Bedeutung |
|---|---|
%s | Unverarbeitete String- oder Byte-Slice-Ausgabe |
%q | Quoted String à la strconv.Quote (mit Escapes) |
%x | Hex-Encoding jedes Bytes, Kleinbuchstaben |
%X | Hex-Encoding jedes Bytes, Großbuchstaben |
%q schützt davor, dass eingebettete Tabs, Zeilenumbrüche oder Steuerzeichen die Lesbarkeit eines Logs zerstören. Ein Newline aus Benutzereingabe erscheint als sichtbares \n, nicht als tatsächlicher Umbruch — Log-Aggregatoren werden es danken.
package main
import "fmt"
func main() {
s := "Hallo\tWelt\n"
b := []byte("Go!")
fmt.Printf("%%s string = %s", s)
fmt.Printf("%%q string = %q\n", s)
fmt.Printf("%%x string = %x\n", s)
fmt.Printf("%%X string = %X\n", s)
fmt.Printf("%%s []byte = %s\n", b)
fmt.Printf("%%x []byte = %x\n", b)
}%s string = Hallo Welt
%q string = "Hallo\tWelt\n"
%x string = 48616c6c6f0957656c740a
%X string = 48616C6C6F0957656C740A
%s []byte = Go!
%x []byte = 476f21%x auf einem String oder []byte ist nicht dasselbe wie %x auf einem Integer: Hier wird jedes Byte hex-encodiert und konkateniert. Das ist die einfachste Art, Binärdaten kompakt zu loggen oder Hashes wie SHA-256 als lesbaren String auszugeben — fmt.Sprintf("%x", h.Sum(nil)) ist ein Klassiker.
Pointer- und Rune-Verben
Pointer haben ein eigenes Verb, weil %v auf einem Pointer den dereferenzierten Wert zeigt — die Adresse selbst bekommt man nur mit %p. Bei Runen überschneidet sich die Funktion mit %c aus dem Integer-Bereich, weil eine Rune in Go schlicht ein int32 ist.
| Verb | Bedeutung |
|---|---|
%p | Pointer-Adresse als 0x...-Hex |
%c | Rune/Codepoint als Zeichen |
%p ist beim Debugging hilfreich, wenn man wissen will, ob zwei Variablen auf dasselbe Objekt zeigen — gleiche Adresse, gleicher Speicher. In normalem Anwendungscode hat %p selten etwas zu suchen, denn Adressen sagen über die Domäne nichts aus.
package main
import "fmt"
func main() {
x := 42
p := &x
fmt.Printf("%%v p = %v\n", p)
fmt.Printf("%%p p = %p\n", p)
fmt.Printf("%%v *p = %v\n", *p)
r := rune('Ω')
fmt.Printf("%%c r = %c\n", r)
fmt.Printf("%%d r = %d\n", r)
fmt.Printf("%%U r = %U\n", r)
}%v p = 0xc0000140a8
%p p = 0xc0000140a8
%v *p = 42
%c r = Ω
%d r = 937
%U r = U+03A9Eine Rune ist im Speicher nichts anderes als eine Zahl, und das spiegelt sich in den Verben: %d zeigt den numerischen Wert, %c das druckbare Zeichen, %U die Unicode-Notation — alle drei beschreiben denselben int32-Wert aus unterschiedlichen Perspektiven.
Bool-Verb — %t
Booleans sind das einfachste Verb-Thema, brauchen aber dennoch eine eigene Erwähnung: Das typisierte Verb für bool heißt %t (für truth) und gibt schlicht true oder false aus. %v liefert dasselbe Ergebnis, weshalb %t in der Praxis selten ausdrücklich benutzt wird — es ist da, falls man explizit signalisieren möchte, dass an dieser Stelle ein Boolean erwartet wird.
package main
import "fmt"
func main() {
ok := true
bad := false
fmt.Printf("%%t = %t\n", ok)
fmt.Printf("%%t = %t\n", bad)
fmt.Printf("%%v = %v\n", ok)
}%t = true
%t = false
%v = trueWer %d auf einen Boolean ansetzt, bekommt keinen Compile-Fehler, sondern die typische Laufzeit-Markierung %!d(bool=true) in die Ausgabe — fmt ist hier streng typisiert, aber tolerant: Es bricht nicht ab, sondern macht den Fehler sichtbar.
Das Wrap-Verb %w
%w ist ein Sonderfall unter allen Verben: Es ist syntaktisch ein Format-Verb, semantisch aber eine Error-Wrapping-Anweisung. Es darf ausschließlich in fmt.Errorf verwendet werden — überall sonst (in Printf, Sprintf, Fprintf) erzeugt es zur Laufzeit den Fehler %!w(...) in der Ausgabe, weil das Verb dort keine Bedeutung hat.
Der Sinn von %w ist, einen Fehler in einen neuen Fehler zu verpacken, ohne die Ursprungs-Information zu verlieren. errors.Is und errors.As können später durch die Kette navigieren und gezielt nach Typ oder Wert suchen. Seit Go 1.20 sind sogar mehrere %w in einem Format-String erlaubt, was Multi-Error-Konstruktionen elegant macht.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
_, err := os.Open("/nicht/da")
wrapped := fmt.Errorf("config laden fehlgeschlagen: %w", err)
fmt.Println(wrapped)
fmt.Println("ist ErrNotExist?", errors.Is(wrapped, os.ErrNotExist))
}config laden fehlgeschlagen: open /nicht/da: no such file or directory
ist ErrNotExist? trueDie Details — Verkettung, Unwrap()-Methoden, Multi-Wrapping, Sentinel-Errors — sind auf der eigenen Seite Error-Wrapping mit %w ausführlich behandelt. Wichtig hier nur: %w gehört in fmt.Errorf, nirgends sonst.
Argument-Index-Notation %[n]verb
Standardmäßig läuft fmt linear durch die Argumentliste: Jedes Verb konsumiert das nächste Argument. Mit der Argument-Index-Notation %[n]verb lässt sich dieser Cursor explizit setzen — n ist die 1-basierte Position. Danach läuft der implizite Cursor von dort weiter.
Das ist selten, aber elegant in zwei Szenarien: Lokalisierung, weil Übersetzungen die Reihenfolge der Platzhalter ändern können, und Wiederholung eines Werts in verschiedenen Darstellungen, ohne das Argument mehrfach übergeben zu müssen.
package main
import "fmt"
func main() {
n := 255
fmt.Printf("%[1]d dez = %[1]b bin = %[1]x hex\n", n)
fmt.Printf("%[2]s wurde von %[1]s eingeladen\n", "Anna", "Ben")
}255 dez = 11111111 bin = ff hex
Ben wurde von Anna eingeladenAchtung: Die Notation gilt nur für das nächste Verb. Nach %[1]d zeigt der Cursor auf Position 2, und das nächste Verb ohne Index nimmt Argument 2. Wer den Cursor erneut auf 1 setzen will, muss [1] wiederholen — was im Beispiel oben dreimal nötig ist.
Spezialfall: nil und Maps
Zwei Themen, bei denen fmt ein wenig anders entscheidet als naiv erwartet: Nil-Werte und Map-Ordering. Ein nil-Pointer wird als <nil> ausgegeben, ein nil-Interface ebenfalls — aber ein typisierter nil-Pointer in einer Interface-Variable zeigt nicht <nil>, sondern den Typnamen, weil das Interface formal nicht-nil ist.
Maps werden seit Go 1.12 mit %v nach Keys sortiert ausgegeben. Davor war die Reihenfolge undefiniert, was Tests, die String-Output verglichen, fragil machte. Heute ist die Ausgabe reproduzierbar — ein kleines, aber sehr nützliches Detail.
package main
import "fmt"
func main() {
var p *int
var i interface{}
var iWithTypedNil interface{} = p
fmt.Printf("nil *int = %v\n", p)
fmt.Printf("nil interface = %v\n", i)
fmt.Printf("typed nil = %v (%T)\n", iWithTypedNil, iWithTypedNil)
m := map[string]int{"zebra": 3, "ant": 1, "mouse": 2}
fmt.Printf("map = %v\n", m)
}nil *int = <nil>
nil interface = <nil>
typed nil = <nil> (*int)
map = map[ant:1 mouse:2 zebra:3]Die sortierte Map-Ausgabe ist Gold wert für Snapshot-Tests und Golden-Files: Man kann sich auf die Reihenfolge verlassen, ohne sort.Strings über die Keys laufen lassen zu müssen. Bei sehr großen Maps kostet die Sortierung allerdings spürbar Zeit — für reine Debug-Ausgaben ist das egal, in heißen Pfaden sollte man auf direktes Iterieren ausweichen.
Praxis 1 — Debug-Output-Cheatsheet
Ein realistisches Beispiel, das die v-Varianten direkt nebeneinander stellt: Eine kleine Datenstruktur mit Pointer, Slice, Map und Interface — formatiert mit %v, %+v und %#v. So wird sichtbar, welches Verb für welches Debugging-Szenario das richtige ist.
package main
import "fmt"
type Address struct {
City string
Zip string
}
type Order struct {
ID int
Customer string
Items []string
Meta map[string]int
Shipping *Address
Payload interface{}
}
func main() {
o := Order{
ID: 7,
Customer: "Anna",
Items: []string{"Buch", "Kaffee"},
Meta: map[string]int{"prio": 1, "weight": 800},
Shipping: &Address{City: "Berlin", Zip: "10115"},
Payload: 3.14,
}
fmt.Printf("%%v = %v\n", o)
fmt.Printf("%%+v = %+v\n", o)
fmt.Printf("%%#v = %#v\n", o)
}%v = {7 Anna [Buch Kaffee] map[prio:1 weight:800] 0xc000010060 3.14}
%+v = {ID:7 Customer:Anna Items:[Buch Kaffee] Meta:map[prio:1 weight:800] Shipping:0xc000010060 Payload:3.14}
%#v = main.Order{ID:7, Customer:"Anna", Items:[]string{"Buch", "Kaffee"}, Meta:map[string]int{"prio":1, "weight":800}, Shipping:(*main.Address)(0xc000010060), Payload:3.14}Auffällig: Der Pointer Shipping wird in allen drei Varianten als Adresse gezeigt, nicht dereferenziert. Wer den inneren Address-Wert sehen will, muss entweder *o.Shipping übergeben oder eine String()-Methode auf Order definieren, die das Feld explizit auflöst. Für die meisten Logging-Zwecke ist %+v die richtige Wahl — kompakt genug für eine Zeile, ausreichend explizit für menschliche Leser.
Praxis 2 — Reference-Card aller wichtigen Verben
Die folgende Tabelle ist als kopierbarer Spickzettel gedacht — alle praxisrelevanten Verben mit einer typischen Eingabe und der erwarteten Ausgabe. Die Reihenfolge folgt der Häufigkeit im Alltag, nicht der alphabetischen Ordnung.
| Verb | Eingabe | Ausgabe | Wofür |
|---|---|---|---|
%v | {Anna 30} | {Anna 30} | Default — fast immer richtig |
%+v | {Anna 30} | {Name:Anna Age:30} | Debug mit Feldnamen |
%#v | {Anna 30} | main.User{Name:"Anna",...} | Go-Syntax, Test-Fixtures |
%T | 7 | int | Type-Discovery bei interface{} |
%d | 255 | 255 | Integer dezimal |
%b | 255 | 11111111 | Integer binär |
%o | 255 | 377 | Integer oktal |
%x | 255 / []byte{0xff} | ff | Integer-Hex / Byte-Hex-Encoding |
%X | 255 | FF | Hex, Großbuchstaben |
%c | 97 | a | Codepoint → Zeichen |
%U | 228 | U+00E4 | Unicode-Notation |
%f | 3.14159 | 3.141590 | Float, 6 Nachkommastellen default |
%e | 3.14159 | 3.141590e+00 | Wissenschaftlich |
%g | 3.14159 | 3.14159 | Float, minimal |
%s | "hallo" | hallo | String pur |
%q | "hallo\n" | "hallo\n" | String quoted, escapt |
%p | &x | 0xc000014080 | Pointer-Adresse |
%t | true | true | Boolean explizit |
%w | err (nur Errorf) | wrapped error | Error-Wrapping |
%% | — | % | Literales Prozentzeichen |
Wer diese Tabelle einmal verinnerlicht hat, schreibt 95 % aller Format-Strings ohne Nachschlagen. Die restlichen 5 % betreffen Modifikatoren wie Breite und Präzision — und die haben eine eigene Referenzseite.
Interessantes
%v, %+v, %#v: drei Detailstufen
%v ist die kompakteste Darstellung — bei Structs ohne Feldnamen. %+v ergänzt die Feldnamen und ist die beste Wahl für Debug-Logs. %#v liefert gültige Go-Syntax und eignet sich, um Laufzeitwerte in Test-Fixtures zu kopieren.
%T ist das Type-Discovery-Verb
Bei interface{}-Werten oder Reflection-Code zeigt %T den konkreten Typ — main.User, *int, []byte, map[string]int. Unverzichtbar, um zur Laufzeit herauszufinden, womit man es eigentlich zu tun hat.
%q escapt Steuerzeichen für Logs
Wo %s einen Tab als echten Tab und einen Newline als Zeilenumbruch ausgibt, zeigt %q sie als \t und \n. Das hält Log-Zeilen einzeilig und schützt vor unerwarteten Umbrüchen durch Benutzereingaben.
%w gehört nur in fmt.Errorf
In Printf, Sprintf oder Fprintf erzeugt %w zur Laufzeit die Markierung %!w(...) in der Ausgabe — kein Compile-Fehler, aber funktional kaputt. Für Wrapping ausschließlich fmt.Errorf verwenden.
%d zwingt Integer — %v ist toleranter
%d auf einen Boolean oder String liefert %!d(typ=wert) zur Laufzeit. %v akzeptiert jeden Typ und sucht sich die passende Darstellung selbst. Wer flexibel formatieren will, nimmt %v; wer den Typ erzwingen will, nimmt das typisierte Verb.
Argument-Index %[n]verb für Wiederholung
%[1]d %[1]b %[1]o zeigt dasselbe Argument in drei Basen, ohne es dreimal übergeben zu müssen. Auch für lokalisierte Templates praktisch, wenn Übersetzungen die Platzhalter-Reihenfolge ändern.
%x ist kontextabhängig
Auf einem Integer liefert %x die hexadezimale Zahl (ff für 255). Auf einem String oder []byte liefert %x das Hex-Encoding jedes Bytes (48656c6c6f für Hello) — zwei verschiedene Operationen mit demselben Verb.
Map-Output ist seit Go 1.12 sortiert
%v auf einer Map gibt die Einträge nach Keys sortiert aus. Vor Go 1.12 war die Reihenfolge undefiniert. Heute lassen sich Snapshot-Tests gegen Map-Output zuverlässig schreiben — ohne manuelles Sortieren.