fmt.Fprintf ist die generalisierte Form von fmt.Printf: derselbe Format-String, dieselben Verben, dieselbe Variadic-Argumentliste — aber das Ziel ist nicht mehr fest verdrahtet auf os.Stdout, sondern wird als erstes Argument übergeben. Jeder Typ, der io.Writer implementiert, kommt in Frage: eine Datei, ein In-Memory-Puffer, eine HTTP-Response, ein gzip-Stream.
Diese kleine Verschiebung — Writer als Parameter statt globaler Konstante — ist einer der Gründe, warum sich Go-Code so gut testen und komponieren lässt. Funktionen, die ihre Ausgabe auf einen übergebenen io.Writer schreiben, sind im Test trivial mit einem bytes.Buffer zu prüfen, in der Produktion können sie gleichzeitig in eine Datei und nach Stdout schreiben.
Signatur — Writer zuerst, Format danach
func Fprintf(w io.Writer, format string, a ...any) (n int, err error). Das Ausgabeziel w steht an erster Stelle, danach folgen Format-String und Werte. Der Rückgabewert ist nicht nur die Anzahl geschriebener Bytes, sondern auch ein error. Bei Printf ignoriert man den Fehler in der Praxis fast immer — Schreibfehler auf Stdout sind selten und meist nicht behandelbar. Bei Fprintf ist das anders, weil der Writer auch eine Datei mit vollem Plattenplatz oder ein abgebrochener Netzwerk-Stream sein kann.
io.Writer — die universelle Schnittstelle
Das Interface io.Writer besteht aus einer einzigen Methode: Write(p []byte) (n int, err error). Diese minimale Schnittstelle ist einer der wirkungsvollsten Designentscheidungen der Go-Standardbibliothek. Implementierungen: os.Stdout, os.Stderr, *os.File, *bytes.Buffer, *strings.Builder, http.ResponseWriter, *gzip.Writer, io.MultiWriter, und viele mehr.
package main
import (
"bytes"
"fmt"
"os"
)
func main() {
name, score := "Anna", 92
fmt.Fprintf(os.Stdout, "Stdout : %s hat %d Punkte\n", name, score)
fmt.Fprintf(os.Stderr, "Stderr : %s hat %d Punkte\n", name, score)
var buf bytes.Buffer
fmt.Fprintf(&buf, "Buffer : %s hat %d Punkte\n", name, score)
fmt.Print(buf.String())
}Stdout : Anna hat 92 Punkte
Stderr : Anna hat 92 Punkte
Buffer : Anna hat 92 PunkteDerselbe Code, dieselbe Logik — nur das Ziel ist anders. Fprintf interessiert sich nicht für die konkrete Implementierung, nur dafür, dass Write existiert.
os.Stderr für Fehler-Output
fmt.Fprintf(os.Stderr, "fehler: %v\n", err) ist der idiomatische Weg, Fehlermeldungen vom normalen Programm-Output zu trennen. Der Unterschied wird relevant, sobald jemand den Output weiterverarbeitet: myprogram | grep foo leitet nur Stdout in grep — Fehlermeldungen erscheinen weiterhin auf dem Terminal.
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
for _, raw := range []string{"42", "abc", "100"} {
n, err := strconv.Atoi(raw)
if err != nil {
fmt.Fprintf(os.Stderr, "fehler: %q ist keine Zahl: %v\n", raw, err)
continue
}
fmt.Fprintf(os.Stdout, "ergebnis: %d\n", n*2)
}
}ergebnis: 84
fehler: "abc" ist keine Zahl: strconv.Atoi: parsing "abc": invalid syntax
ergebnis: 200Pipe-fähig wird das Programm dadurch sofort: ./prog | wc -l zählt nur die Ergebnis-Zeilen.
bytes.Buffer als In-Memory-Writer
bytes.Buffer ist ein wachsender Byte-Puffer, der unter anderem io.Writer implementiert. Für Fprintf heißt das: man kann formatierten Output bauen, ohne ihn sofort an Stdout, Datei oder Netzwerk abzugeben. Erst am Ende wird der gesamte Inhalt als string (buf.String()) oder []byte (buf.Bytes()) abgerufen.
package main
import (
"bytes"
"fmt"
)
type Event struct {
Time string
Level string
Message string
}
func buildLog(events []Event) string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "=== Log-Bericht (%d Einträge) ===\n", len(events))
for i, e := range events {
fmt.Fprintf(&buf, "%2d. [%s] %-5s %s\n", i+1, e.Time, e.Level, e.Message)
}
fmt.Fprintln(&buf, "=== Ende ===")
return buf.String()
}
func main() {
out := buildLog([]Event{
{"09:30:00", "INFO", "Server gestartet"},
{"09:30:02", "WARN", "Cache fast voll"},
{"09:30:05", "ERROR", "Verbindung verloren"},
})
fmt.Print(out)
}=== Log-Bericht (3 Einträge) ===
1. [09:30:00] INFO Server gestartet
2. [09:30:02] WARN Cache fast voll
3. [09:30:05] ERROR Verbindung verloren
=== Ende ===&buf ist nötig, weil Write einen Pointer-Receiver hat. Der zusammengebaute String wird genau einmal am Ende übergeben.
http.ResponseWriter — direkt in die HTTP-Antwort schreiben
Das net/http-Paket übergibt Handlern einen http.ResponseWriter, der ebenfalls io.Writer implementiert. Fprintf ist damit ein direkter Weg, formatierten Body in eine HTTP-Antwort zu schreiben.
package main
import (
"fmt"
"net/http"
)
func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Welt"
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Hallo, %s!\n", name)
fmt.Fprintf(w, "Pfad : %s\n", r.URL.Path)
fmt.Fprintf(w, "Methode: %s\n", r.Method)
}
func main() {
http.HandleFunc("/greet", greetHandler)
http.ListenAndServe(":8080", nil)
}Für strukturierte Antworten (JSON, HTML-Templates) gibt es bessere Werkzeuge, aber für Plain-Text-Responses, Debug-Endpoints und kleine Tools ist Fprintf auf den ResponseWriter das natürlichste Pattern in Go.
In eine Datei schreiben mit *os.File
os.Create liefert ein *os.File, das ebenfalls io.Writer implementiert. Fprintf schreibt damit formatierten Output direkt auf die Platte.
package main
import (
"fmt"
"log"
"os"
)
func main() {
f, err := os.Create("bericht.txt")
if err != nil {
log.Fatalf("create: %v", err)
}
defer f.Close()
fmt.Fprintf(f, "Bericht vom %s\n", "2026-05-21")
fmt.Fprintf(f, "Punkte: %d / %d\n", 92, 100)
fmt.Fprintf(f, "Status: %s\n", "bestanden")
}defer f.Close() direkt nach erfolgreichem Create sichert, dass die Datei am Funktionsende geschlossen wird — ohne Close kann es passieren, dass gepufferte Daten nie auf die Platte gelangen.
Error-Handling ist hier Pflicht
Bei fmt.Printf ignoriert man den zurückgegebenen error in der Praxis fast immer. Bei Fprintf ist die Lage anders: der Writer kann eine Datei sein, deren Plattenplatz voll ist, eine HTTP-Verbindung, die der Client geschlossen hat, oder ein komprimierender Stream, dem ein interner Fehler unterläuft.
package main
import (
"fmt"
"os"
)
func writeReport(path string, score int) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("create %q: %w", path, err)
}
defer f.Close()
if _, err := fmt.Fprintf(f, "Score: %d\n", score); err != nil {
return fmt.Errorf("schreiben fehlgeschlagen: %w", err)
}
return nil
}Für *bytes.Buffer ist der Fehler praktisch immer nil (Buffer wachsen einfach mit). Für Dateien, Netzwerk-Verbindungen und ResponseWriter dagegen ist die Prüfung sinnvoll.
io.MultiWriter — paralleles Schreiben auf mehrere Senken
io.MultiWriter(w1, w2, ...) liefert einen Writer, der jedes Schreiben an alle übergebenen Writer weiterreicht — das Unix-Tool tee als Go-Funktion.
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
f, err := os.Create("run.log")
if err != nil {
log.Fatal(err)
}
defer f.Close()
tee := io.MultiWriter(f, os.Stdout)
fmt.Fprintf(tee, "=== Lauf gestartet ===\n")
fmt.Fprintf(tee, "Verarbeite %d Datensätze\n", 1500)
fmt.Fprintf(tee, "=== Lauf beendet ===\n")
}Die drei Fprintf-Aufrufe schreiben jeweils in run.log UND nach Stdout — der Anwender sieht den Fortschritt live, gleichzeitig bleibt ein dauerhaftes Protokoll auf der Platte.
Saubere Stream-Trennung in einem Web-Handler
In einem HTTP-Handler will man zwei Dinge gleichzeitig: dem Client eine Antwort schicken und eigene Diagnose-Logs für Operations schreiben. Fprintf trennt beides klar.
package main
import (
"fmt"
"net/http"
"os"
"strconv"
"time"
)
func divideHandler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
aStr, bStr := r.URL.Query().Get("a"), r.URL.Query().Get("b")
a, errA := strconv.Atoi(aStr)
b, errB := strconv.Atoi(bStr)
if errA != nil || errB != nil {
fmt.Fprintf(os.Stderr, "[%s] bad input a=%q b=%q\n", r.RemoteAddr, aStr, bStr)
http.Error(w, "ungültige Zahlen", http.StatusBadRequest)
return
}
if b == 0 {
fmt.Fprintf(os.Stderr, "[%s] division by zero\n", r.RemoteAddr)
http.Error(w, "Division durch Null", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d / %d = %d (Rest %d)\n", a, b, a/b, a%b)
fmt.Fprintf(os.Stderr, "[%s] /divide a=%d b=%d ok in %s\n",
r.RemoteAddr, a, b, time.Since(start))
}Der Client bekommt nur das gewünschte Ergebnis; in den Server-Logs landet ein vollständiger Audit-Trail. Beide Streams sind komplett unabhängig.
Testbare Funktionen durch io.Writer-Parameter
Das vielleicht wichtigste Pattern, das Fprintf ermöglicht: Funktionen, die ihre Ausgabe nicht fest auf Stdout schreiben, sondern auf einen übergebenen io.Writer. In der Produktion bekommen sie os.Stdout, im Test einen bytes.Buffer.
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
)
func PrintReport(w io.Writer, name string, scores []int) {
total := 0
for _, s := range scores {
total += s
}
avg := float64(total) / float64(len(scores))
fmt.Fprintf(w, "Schüler : %s\n", name)
fmt.Fprintf(w, "Noten : %v\n", scores)
fmt.Fprintf(w, "Schnitt : %.2f\n", avg)
}
func main() {
PrintReport(os.Stdout, "Anna", []int{85, 92, 78, 90})
fmt.Println(strings.Repeat("-", 30))
var buf bytes.Buffer
PrintReport(&buf, "Test", []int{100, 100, 100})
got := buf.String()
if strings.Contains(got, "Schnitt : 100.00") {
fmt.Println("Test bestanden")
}
}Schüler : Anna
Noten : [85 92 78 90]
Schnitt : 86.25
------------------------------
Test bestandenDieselbe Funktion bedient Produktion und Test, ohne dass irgendetwas umgehängt, gemockt oder global ersetzt werden müsste. Das ist Dependency Injection in ihrer einfachsten Form.
Interessantes
Writer ist erstes Argument
Anders als bei Printf steht der Writer VOR dem Format-String. Das ist Konvention in der gesamten Standardbibliothek.
Error-Handling Pflicht
Anders als bei Printf liefert Fprintf einen echten error zurück — Disk voll, Netzwerk weg, Client disconnected. Bei Datei- und Netzwerk-Targets immer prüfen.
os.Stderr für Diagnose-Output
Fehler und Logs nach os.Stderr, Nutz-Output nach os.Stdout — sonst zerstört man Pipe-Verarbeitung.
bytes.Buffer für In-Memory-Aufbau
Wiederholtes Sprintf mit String-Konkatenation ist langsam. Fprintf auf einen &bytes.Buffer baut den String effizient.
http.ResponseWriter direkt schreibbar
In HTTP-Handlern ist fmt.Fprintf(w, ...) der direkte Weg, Plain-Text-Antworten zu erzeugen.
io.MultiWriter für Tee-Effekt
io.MultiWriter(file, os.Stdout) schreibt jeden Write-Aufruf parallel an beide Ziele.
Funktionen sollten io.Writer akzeptieren
Statt fest auf os.Stdout zu schreiben, einen io.Writer-Parameter nehmen. Produktion: os.Stdout. Test: bytes.Buffer.
log.Logger nutzt intern ähnliches API
Das log-Paket schreibt intern via Writer; seine Printf-Methode arbeitet wie Fprintf auf diesen Writer.