fmt.Printf ist die zentrale Funktion für formatierte Textausgabe in Go. Sie nimmt einen Format-String mit Platzhaltern (Verben wie %d, %s, %v) entgegen, ersetzt diese durch die übergebenen Argumente und schreibt das Ergebnis nach Stdout (os.Stdout). Damit ist sie das Werkzeug erster Wahl für CLI-Output, Debug-Prints und alles, was über simples Println hinausgeht.
Die kanonische Referenz ist die Paket-Dokumentation auf pkg.go.dev/fmt#Printf. Diese Seite ergänzt die offizielle Doku um Praxis-Wissen: typische Fehler, Performance-Charakteristik und die feinen Unterschiede zu den Geschwisterfunktionen Println und Print.
Signatur und Rückgabewerte
Die Funktion ist im fmt-Paket wie folgt deklariert: func Printf(format string, a ...any) (n int, err error). Zwei Rückgabewerte fallen sofort auf: n int ist die Anzahl der tatsächlich geschriebenen Bytes, err error signalisiert einen Schreibfehler des darunterliegenden io.Writer (bei Printf ist das immer os.Stdout). In der Praxis werden beide Werte fast immer ignoriert — bei Stdout/Stderr ist ein Schreibfehler so ungewöhnlich (typischerweise nur bei geschlossener Pipe), dass Behandlung selten lohnt. Wer den Linter beruhigen will, weist explizit dem Blank-Identifier zu.
package main
import "fmt"
func main() {
_, _ = fmt.Printf("Hallo %s\n", "Welt")
_ = fmt.Printf("Bytes geschrieben werden ignoriert\n")
}Hallo Welt
Bytes geschrieben werden ignoriertAnders sieht es aus, wenn man Printf über Fprintf auf einen Netzwerk-Writer, eine Datei oder eine HTTP-Response umlenkt — dort gehört die Fehlerbehandlung zur Sorgfaltspflicht.
Format-String-Grammatik
Ein Format-Verb folgt einer festen Grammatik: %[flags][width][.precision]verb. Alle Teile außer % und dem abschließenden Verb sind optional, die Reihenfolge ist jedoch fix. Das Verb am Ende ist Pflicht — ohne Verb gibt es keinen gültigen Platzhalter.
package main
import "fmt"
func main() {
fmt.Printf("[%d]\n", 42)
fmt.Printf("[%5d]\n", 42)
fmt.Printf("[%-5d]\n", 42)
fmt.Printf("[%05d]\n", 42)
fmt.Printf("[%.2f]\n", 3.14159)
fmt.Printf("[%-10.2s]\n", "Hallo Welt")
fmt.Printf("Rabatt: %d%%\n", 20)
}[42]
[ 42]
[42 ]
[00042]
[3.14]
[Ha ]
Rabatt: 20%Das %%-Pattern ist notwendig, weil ein einzelnes % immer den Beginn eines Verbs markiert. Wer ein literales Prozentzeichen ausgeben will, muss es verdoppeln — alle anderen Zeichen im Format-String werden 1:1 übernommen.
Vergleich zu Println und Print
Das fmt-Paket bietet drei verwandte Stdout-Funktionen, die sich in zwei Dimensionen unterscheiden: Format-String ja/nein und automatische Newline ja/nein. Printf hat den Format-String, hängt aber kein Newline an. Println hat keinen Format-String, ergänzt aber automatisch \n und trennt Argumente mit Leerzeichen. Print macht weder das eine noch das andere.
package main
import "fmt"
func main() {
name, age := "Ada", 36
fmt.Printf("Printf: %s ist %d\n", name, age)
fmt.Println("Println:", name, "ist", age)
fmt.Print("Print: ", name, " ist ", age, "\n")
}Printf: Ada ist 36
Println: Ada ist 36
Print: Ada ist 36Faustregel: Printf wenn Layout-Kontrolle nötig ist, Println für schnelle Debug-Ausgaben mit klarer Zeilentrennung, Print praktisch nie — es bietet keinen Vorteil gegenüber den anderen beiden.
Häufige Verb-Patterns
Vier Verben decken in der Praxis 90 Prozent aller Fälle ab. %v ist der Default-Formatter und funktioniert für jeden Typ — Go entscheidet anhand des Argumenttyps, wie ausgegeben wird. %+v ist die Debug-Variante für Structs und zeigt zusätzlich die Feldnamen. %d, %s, %f sind typspezifisch und zwingen den Typ explizit (was bei Mismatch sichtbar wird). %T druckt den Typ statt des Werts — unverzichtbar beim Debuggen von any-Werten oder Interface-Inhalten.
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Ada", Age: 36}
fmt.Printf("%v\n", u)
fmt.Printf("%+v\n", u)
fmt.Printf("%#v\n", u)
fmt.Printf("%d\n", 42)
fmt.Printf("%s\n", "Hallo")
fmt.Printf("%f\n", 3.14)
fmt.Printf("%T\n", u)
fmt.Printf("%T\n", any(42))
}{Ada 36}
{Name:Ada Age:36}
main.User{Name:"Ada", Age:36}
42
Hallo
3.140000
main.User
int%+v lohnt sich besonders während der Entwicklung: man sieht sofort, welches Feld welchen Wert trägt — bei großen Structs der Unterschied zwischen Rätselraten und schnellem Verstehen.
Newline-Fallen
Der häufigste Printf-Bug bei Go-Einsteigern: vergessenes \n. Anders als println in Sprachen wie Python oder JavaScript hängt Printf kein Newline an — der Format-String bestimmt zu 100 Prozent, was geschrieben wird. Folge: Zwei Printf-Aufrufe ohne \n ergeben eine zusammenhängende Zeile, was selten beabsichtigt ist.
package main
import "fmt"
func main() {
fmt.Printf("Phase 1 fertig")
fmt.Printf("Phase 2 fertig")
fmt.Println()
fmt.Printf("Phase 3 fertig\n")
fmt.Printf("Phase 4 fertig\n")
fmt.Println("Phase 5 fertig")
fmt.Println("Phase 6 fertig")
}Phase 1 fertigPhase 2 fertig
Phase 3 fertig
Phase 4 fertig
Phase 5 fertig
Phase 6 fertigWer eigenständige Zeilen ausgeben will und keinen Format-Bedarf hat, fährt mit Println sicherer — schon weil das \n strukturell nicht vergessen werden kann.
Performance-Charakteristik
Printf ist bequem, aber nicht günstig. Intern nutzt es Reflection, um zur Laufzeit Typ-Informationen aus den any-Argumenten zu extrahieren und gegen die Verben zu matchen. Größenordnung auf moderner Hardware: 30–50 Nanosekunden pro Aufruf für einfache Format-Strings, deutlich mehr bei vielen Argumenten oder komplexen Structs.
Für Hot-Paths — etwa Logging in einer Request-Schleife mit zehntausenden Aufrufen pro Sekunde — sind strings.Builder mit strconv.Itoa/strconv.FormatFloat oder spezialisierte Logger wie log/slog (mit JSON-Handler) um Faktor 3–10 schneller, weil sie ohne Reflection auskommen. Wichtig: Für menschenlesbare CLI-Ausgabe spielt das keine Rolle — Stdout selbst (syscall + Terminal-Rendering) dominiert die Kosten. Die Performance-Frage stellt sich erst, wenn Format-Operationen im heißen Pfad innerhalb der Anwendung stattfinden.
Type-Mismatches
Printf prüft die Verb-Argument-Kombination zur Laufzeit. Passt der Typ nicht zum Verb, gibt es keinen Crash — stattdessen schreibt Printf eine Diagnostik-Marke der Form %!verb(type=value) direkt in den Output. Das ist hilfreich beim Debuggen, aber natürlich kein Verhalten, das in Production-Output gehört.
package main
import "fmt"
func main() {
fmt.Printf("Alter: %d\n", "sechsunddreißig")
fmt.Printf("Name: %s\n", 42)
fmt.Printf("Wert: %f\n", true)
}Alter: %!d(string=sechsunddreißig)
Name: %!s(int=42)
Wert: %!f(bool=true)Die gute Nachricht: go vet flaggt viele dieser Fälle statisch, bevor das Programm überhaupt läuft. go vet ./... ist deshalb fester Bestandteil jeder vernünftigen CI-Pipeline. Der vet-Check printf analysiert Format-Strings und vergleicht Verben mit den übergebenen Argumenttypen — eine der wertvollsten kostenlosen Code-Analysen in Go.
Argument-Anzahl-Mismatches
Neben Typ-Mismatches gibt es auch Mismatches in der Anzahl der Argumente. Zu wenige Argumente führen zu %!verb(MISSING) an der Stelle des fehlenden Werts, zu viele Argumente zu %!(EXTRA type=value) am Ende der Ausgabe. Auch hier gilt: kein Panic, sondern Diagnostik-Marke im Output.
package main
import "fmt"
func main() {
fmt.Printf("a=%d b=%d c=%d\n", 1, 2)
fmt.Printf("a=%d b=%d\n", 1, 2, 3)
fmt.Printf("x=%s y=%s z=%s\n", "ja")
}a=1 b=2 c=%!d(MISSING)
a=1 b=2
%!(EXTRA int=3)
x=ja y=%!s(MISSING) z=%!s(MISSING)go vet warnt auch hier — die Meldung lautet typischerweise Printf format %d reads arg #3, but call has 2 args. Wer vet regelmäßig laufen lässt, sieht diese Klasse von Fehlern nie im Production-Output.
Tabellen-Output ausrichten
Ein klassischer Anwendungsfall für Printf mit Width-Modifikatoren ist tabellarischer Output in CLIs. Linksbündige Strings, rechtsbündige Zahlen, feste Spaltenbreiten — alles in einem Aufruf pro Zeile.
package main
import "fmt"
type Server struct {
Name string
CPU float64
Memory int
}
func main() {
server := []Server{
{"web-01", 12.4, 2048},
{"db-primary", 87.9, 16384},
{"cache-02", 3.1, 512},
}
fmt.Printf("%-12s %8s %10s\n", "NAME", "CPU%", "MEM (MB)")
fmt.Printf("%-12s %8s %10s\n", "----", "----", "--------")
for _, s := range server {
fmt.Printf("%-12s %8.1f %10d\n", s.Name, s.CPU, s.Memory)
}
}NAME CPU% MEM (MB)
---- ---- --------
web-01 12.4 2048
db-primary 87.9 16384
cache-02 3.1 512Der Trick: derselbe Format-String für Header und Datenzeilen — so bleiben die Spalten garantiert aligned, weil dieselben Width-Modifier wirken. Für komplexere Tabellen mit dynamischer Spaltenbreite lohnt das text/tabwriter-Paket aus der Stdlib.
Wann nicht Printf — strukturiertes Logging
Für Debug-Output in der Entwicklung ist Printf schwer zu schlagen. In Production-Code mit strukturierten Logs (JSON, Felder, Log-Level, Korrelations-IDs) stößt es jedoch an Grenzen: Printf-Output ist unstrukturierter Text, den Log-Aggregatoren wie Loki, Elastic oder Datadog nur mit Regex parsen können. Der Go-Weg seit Version 1.21 ist log/slog.
package main
import (
"fmt"
"log/slog"
"os"
)
func main() {
userID := 42
action := "login"
durationMs := 124
fmt.Printf("user %d %s took %dms\n", userID, action, durationMs)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("user action",
slog.Int("user_id", userID),
slog.String("action", action),
slog.Int("duration_ms", durationMs),
)
}user 42 login took 124ms
{"time":"2026-05-21T10:00:00Z","level":"INFO","msg":"user action","user_id":42,"action":"login","duration_ms":124}Die Faustregel: Printf für menschenlesbare CLI-Ausgabe, slog für Server-Logs in Production. Wer beide Welten mischt, riskiert Logs, die für Menschen unleserlich und für Maschinen unbrauchbar sind.
Interessantes
Newline manuell anhängen
Printf hängt kein \n an — wer eigenständige Zeilen will, schreibt es selbst oder greift zu Println.
Rückgabewerte meist ignorierbar
(n int, err error) bei Stdout/Stderr fast immer mit _ = fmt.Printf(...) verworfen — relevant erst bei Fprintf auf Netzwerk/Datei.
%v als Default, %+v für Debug
%v funktioniert für alles, %+v zeigt bei Structs zusätzlich die Feldnamen — Gold beim Entwickeln.
Type-Mismatches als Diagnostik-Marke
Falscher Typ zum Verb erzeugt %!d(string=...) im Output, kein Panic — sichtbar, aber peinlich in Production.
go vet als Sicherheitsnetz
go vet ./... flaggt Verb-Argument-Mismatches statisch — feste Pipeline-Stage, nicht optional.
%% für literales Prozentzeichen
Ein einzelnes % startet immer ein Verb — für ein literales Zeichen muss %% stehen.
Reflection-Kosten beachten
Printf ist 3–10× langsamer als strings.Builder + strconv — für Hot-Paths ohne Format-Bedarf vermeiden.
Width-Modifier für Tabellen
%-12s linksbündig, %8d rechtsbündig, derselbe Format-String für Header und Zeilen — saubere ASCII-Tabellen ohne Extra-Library.