JSON Formatter Go — MarshalIndent() Guide
Nutze das kostenlose JSON Formatter & Beautifier direkt im Browser – keine Installation erforderlich.
JSON Formatter & Beautifier online testen →Wenn ich an einem Go-Microservice arbeite und eine API-Antwort oder eine Konfigurationsdatei inspizieren muss, ist kompaktes JSON das erste Hindernis — eine einzige Zeile mit Hunderten verschachtelter Felder sagt mir auf den ersten Blick fast nichts. Um JSON in Go zu formatieren, bietet die Standardbibliothek alles Notwendige: json.MarshalIndent ist Teil von encoding/json, ist in jeder Go-Installation enthalten und benötigt keinerlei Drittanbieter-Abhängigkeiten. Dieser Leitfaden deckt das gesamte Bild ab: Struct Tags, benutzerdefinierte MarshalJSON()-Implementierungen, json.Indent zum Neuformatieren roher Bytes, das Streaming großer Dateien mit json.Decoder, und wann man für hochdurchsatzkritische Pfade auf go-json zurückgreifen sollte — plus CLI-Einzeiler für schnelle Formatierung im Terminal. Alle Beispiele verwenden Go 1.21+.
- ✓json.MarshalIndent(v, "", "\t") ist Standardbibliothek — null Abhängigkeiten, in jeder Go-Installation enthalten.
- ✓Struct Tags json:"field_name,omitempty" steuern Serialisierungsschlüssel und lassen Nullwert-Felder weg.
- ✓MarshalJSON() auf einem beliebigen Typ implementieren, um die JSON-Darstellung vollständig zu kontrollieren.
- ✓json.Indent() formatiert bereits marshallierte []byte neu, ohne das Struct erneut zu parsen — schneller für rohe Bytes.
- ✓Für große Dateien (>100 MB): json.Decoder mit Token() verwenden, um ohne vollständiges In-Memory-Laden zu streamen.
- ✓go-json ist ein Drop-in-Ersatz, der 3–5× schneller als encoding/json ist — geeignet für High-Throughput-APIs.
Was ist JSON-Formatierung?
JSON-Formatierung — auch Pretty-Printing genannt — wandelt einen kompakten, minimierten JSON-String in ein lesbar strukturiertes Layout mit konsistenter Einrückung und Zeilenumbrüchen um. Die zugrunde liegenden Daten sind identisch; nur der Leerraum ändert sich. Kompaktes JSON ist optimal für die Netzwerkübertragung, wo jedes Byte zählt; formatiertes JSON ist optimal für Debugging, Code-Reviews, Log-Inspektion und das Verfassen von Konfigurationsdateien. Das encoding/json-Paket in Go unterstützt beide Modi mit einem einzigen Funktionsaufruf — zwischen kompakter und eingerückter Ausgabe wechseln durch die Wahl zwischen json.Marshal und json.MarshalIndent.
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — Der Standardbibliothek-Ansatz
json.MarshalIndent lebt im Paket encoding/json, das zur Go-Standardbibliothek gehört — kein go get erforderlich. Die Signatur lautet MarshalIndent(v any, prefix, indent string) ([]byte, error): der prefix-String wird jeder Ausgabezeile vorangestellt (fast immer leer gelassen), und indent wird einmal pro Verschachtelungsebene wiederholt. Man übergibt "\t" für Tabs oder " " für zwei Leerzeichen.
package main
import (
"encoding/json"
"fmt"
"log"
)
type ServiceConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Workers int `json:"workers"`
TLSEnabled bool `json:"tls_enabled"`
AllowedIPs []string `json:"allowed_ips"`
}
func main() {
cfg := ServiceConfig{
Host: "payments-api.internal",
Port: 8443,
Workers: 8,
TLSEnabled: true,
AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("marshal config: %v", err)
}
fmt.Println(string(data))
}
// {
// "host": "payments-api.internal",
// "port": 8443,
// "workers": 8,
// "tls_enabled": true,
// "allowed_ips": [
// "10.0.0.0/8",
// "172.16.0.0/12"
// ]
// }Die Wahl zwischen Tabs und Leerzeichen ist meist eine Teamkonvention. Viele Go-Projekte bevorzugen Tabs, weil gofmt (das Go-Quellcode formatiert) Tabs verwendet. Zwei- oder Vier-Leerzeichen-Einrückung ist üblich, wenn das JSON für einen JavaScript- oder Python-Consumer bestimmt ist. Hier ist dasselbe Struct mit beiden Einrückungsstilen nebeneinander:
// Tab-Einrückung — bevorzugt in Go-nativen Tools
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Zwei-Leerzeichen-Einrückung — gängig für APIs mit JS/Python-Consumern
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Vier-Leerzeichen-Einrückung
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) verwenden, wenn kompakte Ausgabe benötigt wird — Netzwerk-Payloads, Cache-Werte oder jeden Pfad, bei dem die Binärgröße eine Rolle spielt. Es akzeptiert dasselbe Wertargument und hat dieselbe Fehler-Semantik, erzeugt jedoch einzeiliges JSON ohne Leerraum.Struct Tags — Feldnamen und Omitempty steuern
Go-Struct-Tags sind Zeichenkettenliterale, die nach Felddeklarationen stehen undencoding/jsonmitteilen, wie jedes Feld serialisiert werden soll. Es gibt drei Schlüsseldirektiven: json:"name" benennt das Feld in der Ausgabe um, omitempty lässt das Feld weg, wenn es den Nullwert seines Typs enthält, und json:"-" schließt das Feld vollständig aus — nützlich für Passwörter, interne Bezeichner oder Felder, die die Service-Grenze niemals verlassen dürfen.
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // weglassen wenn leerer String
AvatarURL *string `json:"avatar_url,omitempty"` // weglassen wenn nil-Pointer
IsAdmin bool `json:"is_admin,omitempty"` // weglassen wenn false
passwordHash string // nicht exportiert — auto ausgeschlossen
}
// Benutzer mit allen optionalen Feldern befüllt
full := UserProfile{
ID: "usr_7b3c", Email: "l.bauer@beispiel.de",
DisplayName: "Lukas Bauer", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "l.bauer@beispiel.de",
// "display_name": "Lukas Bauer",
// "is_admin": true
// }
// Benutzer ohne optionale Felder — sie werden vollständig weggelassen
minimal := UserProfile{ID: "usr_2a91", Email: "s.mueller@beispiel.de"}
// {
// "id": "usr_2a91",
// "email": "s.mueller@beispiel.de"
// }Der Tag json:"-" ist die richtige Wahl für Felder, die unbedingt ausgeschlossen werden müssen — unabhängig von ihrem Wert — typischerweise Geheimnisse, interne Tracking-Felder oder Daten, die im Speicher korrekt sind, aber niemals an ein externes System serialisiert werden dürfen.
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // niemals serialisiert
RefreshToken string `json:"-"` // niemals serialisiert
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("secret"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey und RefreshToken erscheinen nieencoding/json immer ausgeschlossen, unabhängig von Tags. Es ist nicht nötig, json:"-" zu nicht exportierten Feldern hinzuzufügen — der Ausschluss ist automatisch und kann nicht überschrieben werden.Benutzerdefiniertes MarshalJSON() — Nicht-Standard-Typen behandeln
Jeder Go-Typ kann das Interface json.Marshaler implementieren, indem er eine Methode MarshalJSON() ([]byte, error) definiert. Wenn encoding/json auf einen solchen Typ trifft, ruft es die Methode auf, anstatt sein standardmäßiges reflexionsbasiertes Marshalling zu verwenden. Dies ist das kanonische Go-Muster für Domain-Typen, die eine spezifische Wire-Darstellung benötigen — Geldbeträge, Status-Enums, benutzerdefinierte Zeitformate oder jeden Typ, der Daten anders speichert, als er serialisiert werden soll.
Eigener Typ — Money mit Cent-zu-Dezimal-Konvertierung
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // in Cent gespeichert, um Gleitkomma-Drift zu vermeiden
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Display string `json:"display"`
}{
Amount: float64(m.Amount) / 100,
Currency: m.Currency,
Display: fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
})
}
type Invoice struct {
ID string `json:"id"`
Subtotal Money `json:"subtotal"`
Tax Money `json:"tax"`
Total Money `json:"total"`
}
func main() {
inv := Invoice{
ID: "inv_9a2f91bc",
Subtotal: Money{Amount: 19900, Currency: "EUR"},
Tax: Money{Amount: 1592, Currency: "EUR"},
Total: Money{Amount: 21492, Currency: "EUR"},
}
data, err := json.MarshalIndent(inv, "", " ")
if err != nil {
log.Fatalf("marshal invoice: %v", err)
}
fmt.Println(string(data))
}
// {
// "id": "inv_9a2f91bc",
// "subtotal": { "amount": 199, "currency": "EUR", "display": "EUR 199.00" },
// "tax": { "amount": 15.92, "currency": "EUR", "display": "EUR 15.92" },
// "total": { "amount": 214.92, "currency": "EUR", "display": "EUR 214.92" }
// }Status-Enum — String-Darstellung
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusCancelled
)
var orderStatusNames = map[OrderStatus]string{
StatusPending: "pending",
StatusPaid: "paid",
StatusShipped: "shipped",
StatusCancelled: "cancelled",
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := orderStatusNames[s]
if !ok {
return nil, fmt.Errorf("unknown order status: %d", s)
}
return json.Marshal(name)
}
type Order struct {
ID string `json:"id"`
Status OrderStatus `json:"status"`
}
o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", " ")
// {
// "id": "ord_3c7f",
// "status": "shipped"
// }MarshalJSON() als auch UnmarshalJSON() gemeinsam implementieren. Wenn nur das Marshalling implementiert wird, geht beim Round-Trip des Typs durch JSON (serialisieren → speichern → deserialisieren) stillschweigend Struktur verloren oder es wird der falsche Typ zurückgegeben. Das Paar bildet einen Vertrag, dass der Typ einen JSON-Round-Trip übersteht.UUID — Als String serialisieren
Die Go-Standardbibliothek hat keinen UUID-Typ. Die gängigste Wahl ist github.com/google/uuid, das bereits MarshalJSON() implementiert und als quoted RFC-4122-String serialisiert. Bei Verwendung eines rohen [16]byte oder eines benutzerdefinierten ID-Typs empfiehlt sich die eigene Implementierung des Interfaces, um Base64-kodierte Binär-Blobs in der JSON-Ausgabe zu vermeiden.
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // serialisiert als "550e8400-e29b-41d4-a716-446655440000"
SessionID uuid.UUID `json:"session_id"`
Action string `json:"action"`
ActorID string `json:"actor_id"`
OccuredAt string `json:"occurred_at"`
}
event := AuditEvent{
EventID: uuid.New(),
SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
Action: "user.password_changed",
ActorID: "usr_7f3a91bc",
OccuredAt: "2026-03-10T14:22:00Z",
}
data, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(data))
// {
// "event_id": "a4b2c1d0-...",
// "session_id": "550e8400-e29b-41d4-a716-446655440000",
// "action": "user.password_changed",
// "actor_id": "usr_7f3a91bc",
// "occurred_at": "2026-03-10T14:22:00Z"
// }[16]byte ohne eigenen Typ gespeichert, kodiert encoding/json sie als Base64-String — z. B. "VQ6EAOKbQdSnFkRmVUQAAA==". Immer einen eigenen UUID-Typ verwenden oder MarshalJSON() implementieren, um das kanonische Bindestrich-String-Format auszugeben.json.MarshalIndent() — Parameterreferenz
Die Funktionssignatur lautet func MarshalIndent(v any, prefix, indent string) ([]byte, error). Sowohl prefix als auch indent sind Zeichenkettenliterale — es gibt keine numerische Kurzschreibweise wie Pythons indent=4.
Häufige Struct-Tag-Optionen:
json.Indent() — Bestehende JSON-Bytes neuformatieren
Wenn bereits ein []byte mit JSON vorliegt — etwa aus einem HTTP-Response-Body, einer Postgres- jsonb-Spalte oder einer mit os.ReadFile gelesenen Datei — muss kein Struct definiert und kein Unmarshal durchgeführt werden, bevor formatiert werden kann. json.Indent formatiert die rohen Bytes direkt neu, indem es die eingerückte Ausgabe in einen bytes.Buffer schreibt.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// Simulierter roher JSON-Payload von einem vorgelagerten Service
raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}
// {
// "trace_id": "tr_9a2f",
// "service": "checkout",
// "latency_ms": 342,
// "error": null
// }Ein Muster, das ich in Microservices häufig einsetze: vor dem Schreiben in strukturierte Logs json.Indent aufrufen — es fügt vernachlässigbaren Overhead hinzu und macht Log-Einträge bei einem Incident wesentlich leichter lesbar. Die Funktion ist besonders nützlich für das Logging von HTTP-Antworten, das Pretty-Printing gespeicherter JSON-Strings und Format-on-Read-Pipelines, bei denen die Struct-Definition nicht verfügbar ist.
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// Body ist kein gültiges JSON — roh loggen
logger.Debug("upstream response", "status", statusCode, "body", string(body))
return
}
logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}json.Indent validiert das JSON NICHT vollständig, über das hinaus, was strukturell zum Einfügen von Leerzeichen erforderlich ist. Für eine vollständige Syntaxvalidierung erst json.Valid(data) aufrufen und den false-Fall behandeln, bevor versucht wird einzurücken.JSON aus Datei und HTTP-Antwort formatieren
Zwei der häufigsten Szenarien in Go-Services sind das Formatieren von JSON aus einer Datei auf der Festplatte (Konfigurationsdateien, Fixture-Daten, Migrations-Seeds) und das Pretty-Printing von HTTP-Response-Bodies für Debug-Logging oder Test-Assertions. Beide folgen demselben Muster: Bytes lesen, json.Indent aufrufen oder unmarshalln und dann json.MarshalIndent verwenden, zurückschreiben oder loggen.
Datei lesen → formatieren → zurückschreiben
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
)
func formatJSONFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("invalid JSON in %s", path)
}
var buf bytes.Buffer
if err := json.Indent(&buf, data, "", " "); err != nil {
return fmt.Errorf("indent %s: %w", path, err)
}
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/datenbank.json"); err != nil {
log.Fatalf("format config: %v", err)
}
fmt.Println("config/datenbank.json erfolgreich formatiert")
}HTTP-Antwort → Decode → Pretty-Print für Debug-Logging
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Checks map[string]string `json:"checks"`
UptimeSec int64 `json:"uptime_seconds"`
}
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("health check: %v", err)
}
defer resp.Body.Close()
var result HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("decode health response: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal health response: %v", err)
}
fmt.Printf("Health-Check (%d):
%s
", resp.StatusCode, string(pretty))
}
// Health-Check (200):
// {
// "status": "ok",
// "version": "1.4.2",
// "checks": {
// "database": "ok",
// "cache": "ok",
// "queue": "degraded"
// },
// "uptime_seconds": 172800
// }Roher Response-Body → json.Indent (kein Struct erforderlich)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("read body: %v", err)
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}JSON aus einer HTTP-Antwort in Go formatiert ausgeben
Die beiden oben gezeigten Ansätze decken die häufigsten Fälle ab: in ein typisiertes Struct dekodieren und dann json.MarshalIndent aufrufen (am besten, wenn Felder validiert oder inspiziert werden müssen), oder die rohen Body-Bytes mit io.ReadAll lesen und direkt json.Indent aufrufen (am besten für schnelles Debug-Logging, wenn keine Struct-Definition verfügbar ist). Der Raw-Bytes-Ansatz ist einfacher, bietet aber keine Go-Typsicherheit oder Feldzugriff — es ist eine rein visuelle Transformation. Beide Ansätze verarbeiten große Response-Bodies korrekt, solange der gesamte Body in den Speicher passt.
// Muster A: typisiertes Decode → MarshalIndent // Verwenden, wenn bestimmte Felder inspiziert oder validiert werden müssen var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // Muster B: rohe Bytes → json.Indent // Für schnelles Debug-Logging — keine Struct-Definition erforderlich body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
JSON-Formatierung über die Kommandozeile in Go-Projekten
Manchmal muss ein JSON-Payload direkt im Terminal formatiert werden, ohne ein Go-Programm zu schreiben. Diese Einzeiler haben sich bei mir in der Entwicklung und bei der Incident-Response als unentbehrlich erwiesen.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Nur formatieren cat api-response.json | jq . # Verschachteltes Feld extrahieren cat api-response.json | jq '.checks.database' # Array filtern cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: liest stdin, formatiert, schreibt stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
b,_:=io.ReadAll(os.Stdin)
var buf bytes.Buffer
json.Indent(&buf,b,""," ")
os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.gogofmt formatiert Go-Quellcode, nicht JSON. JSON-Dateien nicht durch gofmt pipen — es wird entweder einen Fehler erzeugen oder unlesbaren Output produzieren. Für JSON-Dateien jq . oder python3 -m json.tool verwenden.Hochperformante Alternative — go-json
Für die überwiegende Mehrheit der Go-Services ist encoding/json schnell genug. Aber wenn JSON-Marshalling im Profiler auftaucht — häufig bei hochdurchsatzstarken REST-APIs oder Services, die bei jeder Anfrage große strukturierte Log-Zeilen ausgeben — ist die Bibliothek go-json ein Drop-in-Ersatz, der 3–5× schneller ist und dieselbe API-Oberfläche bietet.
go get github.com/goccy/go-json
package main
import (
// Ersetze dies:
// "encoding/json"
// Durch dies — identische API, keine weiteren Codeänderungen:
json "github.com/goccy/go-json"
"fmt"
"log"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
IPAddress string `json:"ip_address"`
DurationMs int `json:"duration_ms"`
}
func main() {
event := AuditEvent{
RequestID: "req_7d2e91", UserID: "usr_4421",
Action: "rechnung.download", ResourceID: "inv_9a2f",
IPAddress: "203.0.113.45", DurationMs: 23,
}
data, err := json.MarshalIndent(event, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// Ausgabe ist identisch mit encoding/json — nur die Geschwindigkeit unterscheidet sich
}github.com/bytedance/sonic ist die schnellste verfügbare Go-JSON-Bibliothek, läuft aber nur auf amd64 und arm64 (sie verwendet JIT-Kompilierung). Man greift auf go-json zurück, wenn ein portabler Drop-in benötigt wird; auf sonic dann, wenn man auf einer bekannten Architektur ist und in einem Hot Path jede Mikrosekunde zählt.
Mit großen JSON-Dateien arbeiten
json.MarshalIndent und json.Indent erfordern beide, dass der gesamte Payload im Speicher liegt. Für Dateien über 100 MB — Daten-Exports, Audit-Logs, Kafka-Consumer-Payloads — sollte json.Decoder verwendet werden, um den Input zu streamen und Datensätze einzeln zu verarbeiten.
Großes JSON-Array mit json.Decoder streamen
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Severity string `json:"severity"`
DurationMs int `json:"duration_ms"`
}
func processAuditLog(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// Öffnendes '[' lesen
if _, err := dec.Token(); err != nil {
return fmt.Errorf("read opening token: %w", err)
}
var processed int
for dec.More() {
var event AuditEvent
if err := dec.Decode(&event); err != nil {
return fmt.Errorf("decode event %d: %w", processed, err)
}
// Ein Event nach dem anderen verarbeiten — konstanter Speicherverbrauch
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("%d Audit-Events verarbeitet
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("process audit log: %v", err)
}
}NDJSON — Ein JSON-Objekt pro Zeile
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type LogLine struct {
Timestamp string `json:"ts"`
Level string `json:"level"`
Service string `json:"service"`
Message string `json:"msg"`
TraceID string `json:"trace_id"`
DurationMs int `json:"duration_ms,omitempty"`
}
func main() {
file, err := os.Open("service-2026-03.ndjson")
if err != nil {
log.Fatalf("open log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB pro Zeile
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // fehlerhafte Zeilen überspringen
}
if line.Level == "error" {
fmt.Printf("%s [%s] %s trace=%s
",
line.Timestamp, line.Service, line.Message, line.TraceID)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scan: %v", err)
}
}os.ReadFile alloziert den gesamten Buffer auf dem Heap, erzeugt GC-Druck und kann auf speicherbeschränkten Containern zu OOM führen.Häufige Fehler
Problem: Den Fehler mit _ zu verwerfen bedeutet, dass ein nicht serialisierbarer Wert (ein Channel, eine Funktion, eine komplexe Zahl) stillschweigend nil-Output produziert oder weiter unten beim Aufruf von string(nil) zu einem Panic führt.
Lösung: Den Fehler immer prüfen. Wenn ein Typ marshalliert wird, der immer erfolgreich sein sollte, ist ein Panic mit log.Fatalf besser als stiller Datenverlust.
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // leerer String wenn Marshal fehlschlug
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))Problem: fmt.Println(string(data)) hängt ein Zeilenumbruchzeichen nach dem JSON an, was Pipelines korrumpiert, die die Ausgabe als rohe Bytes behandeln — zum Beispiel beim Pipen zu jq oder beim Schreiben in ein binäres Protokoll.
Lösung: os.Stdout.Write(data) für binär-saubere Ausgabe verwenden. Wenn ein abschließender Zeilenumbruch für die menschliche Darstellung benötigt wird, explizit anhängen.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // fügt einen zusätzlichen Zeilenumbruch am Ende hinzu
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // expliziter Zeilenumbruch nur wenn nötigProblem: Ohne omitempty serialisiert ein nil-*string- oder *int-Pointer als "field": null. Das legt interne Feldnamen offen und kann strenge JSON-Schema-Validatoren auf der Consumer-Seite brechen.
Lösung: omitempty zu Pointer-Feldern hinzufügen, die in der Ausgabe fehlen (nicht null) sein sollen. Ein nil-*T mit omitempty erzeugt überhaupt keinen Schlüssel im JSON.
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // erscheint als null wenn nil
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // weggelassen wenn nil
}
// {"event_id":"evt_3c7f"}Problem: Das Unmarshallen in map[string]any verliert Typinformationen, erfordert manuelle Type-Assertions und erzeugt nicht-deterministische Schlüsselreihenfolge — was JSON-Diffs und Log-Vergleiche schwieriger macht.
Lösung: Ein Struct mit korrekten json-Tags definieren. Structs sind typsicher, marshallen schneller, erzeugen deterministische Feldreihenfolge entsprechend der Struct-Definition und machen den Code selbstdokumentierend.
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // Type-Assertion erforderlich, Panic bei falschem Typ
type ServiceStatus struct {
Service string `json:"service"`
Port int `json:"port"`
Healthy bool `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // typisiert, sicher, schnellencoding/json vs. Alternativen — Kurzvergleich
json.MarshalIndent verwenden, wenn die Struct-Definition kontrolliert wird und formatierte Ausgabe benötigt wird — Konfigurationsdateien, Debug-Logging, Test-Fixtures und API-Response-Logging. json.Indent verwenden, wenn bereits rohe Bytes vorliegen und nur Leerraum hinzugefügt werden muss, ohne einen Umweg über Go-Typen. Zu go-json oder sonic wechseln erst, nachdem das Profiling bestätigt hat, dass JSON-Marshalling ein messbarer Engpass ist — für die meisten Services ist die Standardbibliothek mehr als ausreichend.
Häufig gestellte Fragen
Wie gibt man JSON in Go formatiert aus?
Mit json.MarshalIndent(v, "", "\t") aus dem Paket encoding/json — das zweite Argument ist ein zeilenweises Präfix (meist leer) und das dritte die Einrückung pro Ebene. "\t" für Tabs oder " " für zwei Leerzeichen. Eine externe Bibliothek ist nicht erforderlich; encoding/json gehört zur Go-Standardbibliothek.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "eu-central-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "eu-central-1",
// "service": "payments-api"
// }
}Was ist der Unterschied zwischen json.Marshal und json.MarshalIndent?
json.Marshal erzeugt kompaktes einzeiliges JSON ohne Leerzeichen — ideal für die Netzwerkübertragung, wo jedes Byte zählt. json.MarshalIndent akzeptiert zwei zusätzliche String-Parameter (prefix und indent) und erzeugt eingerückte, lesbare Ausgabe. Beide Funktionen akzeptieren dieselben Werttypen und geben ([]byte, error) zurück. Der einzige Mehraufwand von MarshalIndent sind etwas mehr Ausgabe-Bytes und eine vernachlässigbare CPU-Last für das Einfügen von Leerzeichen.
import "encoding/json"
type HealthCheck struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime int `json:"uptime_seconds"`
}
h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}
compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}
pretty, _ := json.MarshalIndent(h, "", " ")
// {
// "status": "ok",
// "version": "1.4.2",
// "uptime_seconds": 86400
// }Wie formatiere ich ein JSON-[]byte ohne das Struct zu unmarshallen?
Mit json.Indent(&buf, src, "", "\t"). Diese Funktion nimmt ein vorhandenes []byte mit JSON und schreibt die eingerückte Version in einen bytes.Buffer — ohne Struct-Definition, ohne Type-Assertion, ohne Umweg über Go-Typen. Es ist die schnellste Option, wenn bereits rohe JSON-Bytes vorliegen, etwa aus einem HTTP-Response-Body oder einer Datenbankspalte.
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/rechnungen","page":1,"per_page":50,"total":312}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// "endpoint": "/api/v2/rechnungen",
// "page": 1,
// "per_page": 50,
// "total": 312
// }Warum gibt json.MarshalIndent einen Fehler zurück?
encoding/json gibt einen Fehler zurück, wenn der Wert nicht als JSON dargestellt werden kann. Die häufigsten Ursachen: Ein Channel, eine Funktion oder eine komplexe Zahl wird marshalliert (diese haben kein JSON-Äquivalent); ein Struct, das MarshalJSON() implementiert und einen Fehler zurückgibt; oder eine Map mit Nicht-String-Schlüsseln. Wichtig: Das Marshallieren eines Structs mit einem nicht exportierten oder nil-Pointer-Feld verursacht keinen Fehler — diese werden einfach weggelassen.
import (
"encoding/json"
"fmt"
)
// Dies gibt einen Fehler — Channels sind nicht JSON-serialisierbar
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// Dies ist in Ordnung — nil-Pointer-Felder mit omitempty werden stillschweigend weggelassen
type Profile struct {
ID string `json:"id"`
Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", " ")
// {"id": "usr_7b3c"} — avatar weggelassen, kein FehlerWie schließe ich ein Feld von der JSON-Ausgabe in Go aus?
Es gibt drei Möglichkeiten. Erstens: Den Struct-Tag json:"-" verwenden — das Feld wird unabhängig von seinem Wert immer ausgeschlossen. Zweitens: omitempty verwenden — das Feld wird nur ausgeschlossen, wenn es den Nullwert seines Typs enthält (nil-Pointer, leerer String, 0, false). Drittens: Nicht exportierte (kleingeschriebene) Felder werden von encoding/json automatisch ausgeschlossen, ohne dass ein Tag benötigt wird.
type Zahlungsmethode struct {
ID string `json:"id"`
Last4 string `json:"last4"`
AblaufMonat int `json:"expiry_month"`
AblaufJahr int `json:"expiry_year"`
CVV string `json:"-"` // immer ausgeschlossen
Rechnungsname string `json:"billing_name,omitempty"` // ausgeschlossen wenn leer
internalRef string // nicht exportiert — auto ausgeschlossen
}
pm := Zahlungsmethode{
ID: "pm_9f3a", Last4: "4242",
AblaufMonat: 12, AblaufJahr: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, Rechnungsname (leer) und internalRef erscheinen nicht in der AusgabeWie verarbeite ich time.Time beim JSON-Marshalling?
encoding/json marshalliert time.Time standardmäßig im Format RFC3339Nano (z. B. "2026-03-10T14:22:00Z"), das mit ISO 8601 kompatibel ist. Wenn ein anderes Format benötigt wird — etwa Unix-Epoch-Integer für eine Legacy-API oder ein benutzerdefinierter Datumsstring — implementiert man MarshalJSON() auf einem Wrapper-Typ, der time.Time einbettet und das gewünschte Format zurückgibt.
import (
"encoding/json"
"fmt"
"time"
)
// Standardverhalten — RFC3339Nano, kein eigener Code nötig
type AuditEvent struct {
Aktion string `json:"action"`
EingetratenAm time.Time `json:"occurred_at"`
}
e := AuditEvent{
Aktion: "rechnung.bezahlt",
EingetratenAm: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "rechnung.bezahlt",
// "occurred_at": "2026-03-10T14:22:00Z"
// }
// Benutzerdefiniert: Unix-Zeitstempel als Integer
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}Verwandte Tools
James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.
Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.