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.

Go ignore_return.go
package main

import "fmt"

func main() {
	_, _ = fmt.Printf("Hallo %s\n", "Welt")
	_ = fmt.Printf("Bytes geschrieben werden ignoriert\n")
}
Output
Hallo Welt
Bytes geschrieben werden ignoriert

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

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

Go vergleich.go
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")
}
Output
Printf:  Ada ist 36
Println: Ada ist 36
Print:   Ada ist 36

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

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

Go newline_bug.go
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")
}
Output
Phase 1 fertigPhase 2 fertig
Phase 3 fertig
Phase 4 fertig
Phase 5 fertig
Phase 6 fertig

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

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

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

Go tabelle.go
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)
	}
}
Output
NAME             CPU%   MEM (MB)
----             ----   --------
web-01           12.4       2048
db-primary       87.9      16384
cache-02          3.1        512

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

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

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht