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.

Go drei_writer.go
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())
}
Output
Stdout : Anna hat 92 Punkte
Stderr : Anna hat 92 Punkte
Buffer : Anna hat 92 Punkte

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

Go stderr_fehler.go
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)
	}
}
Output
ergebnis: 84
fehler: "abc" ist keine Zahl: strconv.Atoi: parsing "abc": invalid syntax
ergebnis: 200

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

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

Go http_handler.go
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.

Go datei_schreiben.go
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.

Go fprintf_error.go
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.

Go multiwriter.go
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.

Go praxis_handler.go
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.

Go praxis_test.go
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")
	}
}
Output
Schüler  : Anna
Noten     : [85 92 78 90]
Schnitt   : 86.25
------------------------------
Test bestanden

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

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht