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.

VerbBedeutung
%vDefault-Format — kompakteste sinnvolle Darstellung
%+vWie %v, aber bei Structs mit Feldnamen
%#vGo-Syntax-Repräsentation (so wie im Quelltext)
%TTypname 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.

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

VerbBedeutung
%dDezimal (Basis 10) — der Default für Integer
%bBinär (Basis 2)
%oOktal (Basis 8)
%OOktal mit Präfix 0o
%xHexadezimal, Kleinbuchstaben (ff)
%XHexadezimal, Großbuchstaben (FF)
%cZeichen, das dem Unicode-Codepoint entspricht
%UUnicode-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.

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

VerbBedeutung
%fDezimal ohne Exponent (3.141593)
%FSynonym für %f
%eWissenschaftlich, kleiner Exponent (3.141593e+00)
%EWissenschaftlich, großer Exponent (3.141593E+00)
%g%e für große/kleine Exponenten, sonst %f — minimal
%GWie %g, aber mit großem E
%bDezimal-loses binäres Exponentialformat (Debug-Format)
%xHexadezimaler Float (0x1.921fb54442d18p+01)
%XHexadezimaler 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.

Go float_verbs.go
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)
}
Output
%f = 3.141593
%e = 3.141593e+00
%E = 3.141593E+00
%g = 3.1415926535897931
%G = 3.1415926535897931
%b = 7074237752028440p-51
%x = 0x1.921fb54442d18p+01

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

VerbBedeutung
%sUnverarbeitete String- oder Byte-Slice-Ausgabe
%qQuoted String à la strconv.Quote (mit Escapes)
%xHex-Encoding jedes Bytes, Kleinbuchstaben
%XHex-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.

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

VerbBedeutung
%pPointer-Adresse als 0x...-Hex
%cRune/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.

Go pointer_rune_verbs.go
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)
}
Output
%v p  = 0xc0000140a8
%p p  = 0xc0000140a8
%v *p = 42
%c r  = Ω
%d r  = 937
%U r  = U+03A9

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

Go bool_verb.go
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)
}
Output
%t = true
%t = false
%v = true

Wer %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.

Go wrap_verb.go
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))
}
Output
config laden fehlgeschlagen: open /nicht/da: no such file or directory
ist ErrNotExist? true

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

Go argument_index.go
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")
}
Output
255 dez = 11111111 bin = ff hex
Ben wurde von Anna eingeladen

Achtung: 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.

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

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

VerbEingabeAusgabeWofü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
%T7intType-Discovery bei interface{}
%d255255Integer dezimal
%b25511111111Integer binär
%o255377Integer oktal
%x255 / []byte{0xff}ffInteger-Hex / Byte-Hex-Encoding
%X255FFHex, Großbuchstaben
%c97aCodepoint → Zeichen
%U228U+00E4Unicode-Notation
%f3.141593.141590Float, 6 Nachkommastellen default
%e3.141593.141590e+00Wissenschaftlich
%g3.141593.14159Float, minimal
%s"hallo"halloString pur
%q"hallo\n""hallo\n"String quoted, escapt
%p&x0xc000014080Pointer-Adresse
%ttruetrueBoolean explizit
%werr (nur Errorf)wrapped errorError-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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht