JSON Formatter Go — Guida MarshalIndent()

·Systems Engineer·Revisionato daTobias Müller·Pubblicato

Usa il Formattatore e Abbellitore JSON gratuito direttamente nel tuo browser — nessuna installazione.

Prova Formattatore e Abbellitore JSON online →

Quando lavoro su un microservizio Go e ho bisogno di ispezionare una risposta API o un file di configurazione, il JSON compatto è il primo ostacolo — una singola riga con centinaia di campi annidati non mi dice quasi nulla a colpo d'occhio. Per formattare JSON in Go, la libreria standard offre tutto il necessario: json.MarshalIndent è integrato in encoding/json, incluso in ogni installazione di Go e non richiede dipendenze esterne. Questa guida copre il quadro completo: struct tags, implementazioni personalizzate di MarshalJSON() , json.Indent per riformattare byte grezzi, streaming di file grandi con json.Decoder, e quando ricorrere a go-json per percorsi ad alto throughput, oltre a comandi one-liner per la formattazione rapida nel terminale. Tutti gli esempi usano Go 1.21+.

  • json.MarshalIndent(v, "", "\t") è della libreria standard — zero dipendenze, incluso in ogni installazione Go.
  • I struct tag json:"field_name,omitempty" controllano le chiavi di serializzazione e omettono i campi con valore zero.
  • Implementa MarshalJSON() su qualsiasi tipo per controllare completamente la sua rappresentazione JSON.
  • json.Indent() riformatta []byte già serializzati senza ri-parsare la struct — più veloce per i byte grezzi.
  • Per file grandi (>100 MB): usa json.Decoder con Token() per lo streaming senza caricare tutto in memoria.
  • go-json è un sostituto diretto 3–5× più veloce di encoding/json per API ad alto throughput.

Cos'è la formattazione JSON?

La formattazione JSON — chiamata anche pretty-printing — trasforma una stringa JSON compatta e minificata in un layout leggibile con indentazione e interruzioni di riga coerenti. I dati sottostanti sono identici; cambia solo lo spazio bianco. Il JSON compatto è ottimale per il trasferimento di rete dove ogni byte conta; il JSON formattato è ottimale per il debug, la revisione del codice, l'ispezione dei log e la creazione di file di configurazione. Il package encoding/json di Go gestisce entrambe le modalità con una singola chiamata a funzione — alterna tra output compatto e indentato scegliendo tra json.Marshal e json.MarshalIndent.

Before · json
After · json
{"service":"payments","port":8443,"workers":4}
{
	"service": "payments",
	"port": 8443,
	"workers": 4
}

json.MarshalIndent() — L'approccio della libreria standard

json.MarshalIndent si trova nel package encoding/json , che fa parte della libreria standard di Go — nessun go get necessario. La sua firma è MarshalIndent(v any, prefix, indent string) ([]byte, error): la stringa prefix viene anteposta a ogni riga di output (quasi sempre lasciata vuota), e indent viene ripetuta una volta per livello di annidamento. Passa "\t" per le tabulazioni o " " per due spazi.

Go — esempio minimo funzionante
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"
// 	]
// }

La scelta tra tabulazioni e spazi è principalmente una convenzione di team. Molti progetti Go preferiscono le tabulazioni perché gofmt (che formatta il codice sorgente Go) le utilizza. L'indentazione a due o quattro spazi è comune quando il JSON è destinato a un consumatore JavaScript o Python. Ecco la stessa struct con entrambi gli stili di indentazione affiancati:

Go — tabulazioni vs spazi
// Indentazione con tabulazioni — preferita negli strumenti nativi Go
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Indentazione a due spazi — comune per API con consumatori JS/Python
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Indentazione a quattro spazi
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Nota:Usa json.Marshal(v) quando hai bisogno di output compatto — payload di rete, valori in cache o qualsiasi percorso dove la dimensione in byte conta. Accetta lo stesso argomento di valore e ha la stessa semantica degli errori, ma produce JSON su una singola riga senza spazi bianchi.

Struct Tags — Controllare nomi di campo e omitempty

I struct tag di Go sono letterali di stringa posizionati dopo le dichiarazioni di campo che indicano a encoding/jsoncome serializzare ogni campo. Ci sono tre direttive chiave: json:"name" rinomina il campo nell'output, omitempty omette il campo quando contiene il valore zero del suo tipo, e json:"-" esclude il campo completamente — utile per password, identificatori interni o campi che non devono mai uscire dal confine del servizio.

Go — struct tags per una risposta API
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // omettere se stringa vuota
	AvatarURL   *string `json:"avatar_url,omitempty"`    // omettere se puntatore nil
	IsAdmin     bool    `json:"is_admin,omitempty"`      // omettere se false
	passwordHash string                                   // non esportato — escluso automaticamente
}

// Utente con tutti i campi opzionali popolati
full := UserProfile{
	ID: "usr_7b3c", Email: "m.rossi@esempio.it",
	DisplayName: "Marco Rossi", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "m.rossi@esempio.it",
//   "display_name": "Marco Rossi",
//   "is_admin": true
// }

// Utente senza campi opzionali — vengono omessi completamente
minimal := UserProfile{ID: "usr_2a91", Email: "s.bianchi@esempio.it"}
// {
//   "id": "usr_2a91",
//   "email": "s.bianchi@esempio.it"
// }

Il tag json:"-" è la scelta giusta per i campi che devono essere esclusi incondizionatamente indipendentemente dal loro valore — tipicamente segreti, campi di tracciamento interno o dati corretti in memoria che non devono mai essere serializzati verso alcun sistema esterno.

Go — escludere campi sensibili
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // mai serializzato
	RefreshToken string `json:"-"`   // mai serializzato
}

tok := AuthToken{
	TokenID: "tok_8f2a", Subject: "usr_7b3c",
	IssuedAt: 1741614120, ExpiresAt: 1741617720,
	SigningKey: []byte("segreto"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", "  ")
// {
//   "token_id": "tok_8f2a",
//   "sub": "usr_7b3c",
//   "iat": 1741614120,
//   "exp": 1741617720
// }
// SigningKey e RefreshToken non appaiono mai
Nota:I campi di struct non esportati (in minuscolo) sono sempre esclusi da encoding/json indipendentemente da qualsiasi tag. Non è necessario aggiungere json:"-"ai campi non esportati — l'esclusione è automatica e non può essere sovrascritta.

MarshalJSON() personalizzato — Gestire tipi non standard

Qualsiasi tipo Go può implementare l'interfaccia json.Marshaler definendo un metodo MarshalJSON() ([]byte, error). Quando encoding/json incontra un tale tipo, chiama il metodo invece della sua serializzazione basata su reflection predefinita. Questo è il pattern Go canonico per i tipi di dominio che necessitano di una rappresentazione di rete specifica — valori monetari, enum di stato, formati di data personalizzati o qualsiasi tipo che memorizza i dati in modo diverso da come devono essere serializzati.

Tipo personalizzato — Money con conversione centesimi in decimale

Go — MarshalJSON personalizzato per Money
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Money struct {
	Amount   int64  // memorizzato in centesimi per evitare drift in virgola mobile
	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" }
// }

Enum di stato — Rappresentazione come stringa

Go — MarshalJSON personalizzato per enum di stato
type OrderStatus int

const (
	StatusPending OrderStatus = iota
	StatusPaid
	StatusShipped
	StatusCancelled
)

var orderStatusNames = map[OrderStatus]string{
	StatusPending:   "in_attesa",
	StatusPaid:      "pagato",
	StatusShipped:   "spedito",
	StatusCancelled: "annullato",
}

func (s OrderStatus) MarshalJSON() ([]byte, error) {
	name, ok := orderStatusNames[s]
	if !ok {
		return nil, fmt.Errorf("stato ordine sconosciuto: %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": "spedito"
// }
Nota:Implementa sempre MarshalJSON() e UnmarshalJSON() insieme. Se implementi solo la serializzazione, fare un ciclo completo del tipo attraverso JSON (serializzare → memorizzare → deserializzare) perderà silenziosamente struttura o restituirà il tipo sbagliato. La coppia forma un contratto che garantisce che il tipo possa sopravvivere a un ciclo JSON completo.

UUID — Serializzare come stringa

La libreria standard di Go non ha un tipo UUID. La scelta più comune è github.com/google/uuid, che implementa già MarshalJSON() e serializza come una stringa RFC 4122 tra virgolette. Se usi un [16]byte grezzo o un tipo ID personalizzato, implementa l'interfaccia tu stesso per evitare blob binari codificati in base64 nell'output JSON.

Go 1.21+ — serializzazione UUID
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // serializzato come "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:    "utente.password_modificata",
    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": "utente.password_modificata",
//   "actor_id": "usr_7f3a91bc",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }
Nota:Se memorizzi UUID come [16]byte senza un tipo personalizzato, encoding/json li codifica come stringa base64 — es. "VQ6EAOKbQdSnFkRmVUQAAA==". Usa sempre un tipo UUID appropriato o implementa MarshalJSON() per emettere il formato canonico di stringa con trattini.

Riferimento parametri di json.MarshalIndent()

La firma della funzione è func MarshalIndent(v any, prefix, indent string) ([]byte, error). Sia prefix che indent sono letterali di stringa — non esiste una scorciatoia numerica come l' indent=4 di Python.

Parametro
Tipo
Descrizione
v
any
Il valore da serializzare — struct, map, slice o primitivo
prefix
string
Stringa anteposta a ogni riga di output (di solito "")
indent
string
Stringa usata per ogni livello di indentazione ("\t" o " ")

Opzioni comuni di struct tag:

Tag
Effetto
json:"name"
Rinomina il campo in name nell'output JSON
json:"name,omitempty"
Rinomina + omette se valore zero (nil, "", 0, false)
json:"-"
Esclude sempre questo campo dall'output JSON
json:",string"
Codifica un numero o bool come valore stringa JSON

json.Indent() — Riformattare byte JSON esistenti

Quando hai già un []byte di JSON — ad esempio dal corpo di una risposta HTTP, una colonna jsonb Postgres, o un file letto con os.ReadFile — non hai bisogno di definire una struct e fare unmarshal prima di poter formattare. json.Indent riformatta direttamente i byte grezzi scrivendo l'output indentato in un bytes.Buffer.

Go — json.Indent su byte grezzi
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	// Simulazione di un payload JSON grezzo da un servizio upstream
	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
// }

Un pattern che uso frequentemente nei microservizi è chiamare json.Indent prima di scrivere nei log strutturati — aggiunge un overhead trascurabile e rende le voci di log molto più facili da leggere durante un incidente. La funzione è particolarmente utile per registrare risposte HTTP, formattare stringhe JSON memorizzate e pipeline di format-on-read dove la definizione della struct non è disponibile.

Go — json.Indent per il logging di debug
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Il corpo non è JSON valido — registrare grezzo
		logger.Debug("risposta upstream", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("risposta upstream", "status", statusCode, "body", pretty.String())
}
Attenzione:json.Indent NON valida completamente il JSON oltre a ciò che è strutturalmente necessario per inserire gli spazi bianchi. Per la validazione sintattica completa, chiama json.Valid(data)prima e gestisci il caso false prima di tentare l'indentazione.

Formattare JSON da un file e da una risposta HTTP

Due degli scenari più comuni nei servizi Go sono la formattazione di JSON letto da un file su disco (file di configurazione, dati di fixture, seed di migrazione) e la formattazione di corpi di risposta HTTP per il logging di debug o le asserzioni nei test. Entrambi seguono lo stesso pattern: leggere i byte, chiamare json.Indent o fare unmarshal e poi json.MarshalIndent, riscrivere o registrare.

Leggere file → Formattare → Riscrivere

Go — formattare un file JSON sul posto
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("leggere %s: %w", path, err)
	}

	if !json.Valid(data) {
		return fmt.Errorf("JSON non valido 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("scrivere %s: %w", path, err)
	}
	return nil
}

func main() {
	if err := formatJSONFile("config/database.json"); err != nil {
		log.Fatalf("formattare config: %v", err)
	}
	fmt.Println("config/database.json formattato con successo")
}

Risposta HTTP → Decodificare → Formattare per il logging di debug

Go — formattare una risposta HTTP per il log di debug
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("decodificare risposta health: %v", err)
	}

	pretty, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("marshal risposta health: %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
// }

Corpo risposta grezzo → json.Indent (senza struct necessaria)

Go — formattare corpo HTTP grezzo con io.ReadAll
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("leggere corpo: %v", err)
	}

	var buf bytes.Buffer
	if err := json.Indent(&buf, body, "", "  "); err != nil {
		log.Fatalf("indent: %v", err)
	}
	fmt.Println(buf.String())
}

Visualizzare JSON formattato da una risposta HTTP in Go

I due approcci precedenti coprono i casi più comuni: decodificare in una struct tipizzata e poi chiamare json.MarshalIndent (meglio quando devi validare o ispezionare campi specifici), oppure leggere i byte grezzi del corpo con io.ReadAll e chiamare direttamente json.Indent (meglio per il logging di debug rapido quando non hai una definizione di struct a portata di mano). L'approccio con byte grezzi è più semplice ma non fornisce la sicurezza dei tipi Go né l'accesso ai campi — è puramente una trasformazione di visualizzazione. Entrambi gli approcci gestiscono correttamente corpi di risposta grandi finché il corpo completo rientra nella memoria.

Go — due pattern affiancati
// Pattern A: decodifica tipizzata → MarshalIndent
// Usa quando devi ispezionare o validare campi specifici
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Pattern B: byte grezzi → json.Indent
// Usa per logging di debug rapido — senza definizione di struct necessaria
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Formattazione JSON da riga di comando nei progetti Go

A volte hai bisogno di formattare un payload JSON direttamente nel terminale senza scrivere un programma Go. Questi comandi one-liner sono quelli che ho in memoria muscolare durante lo sviluppo e la risposta agli incidenti.

bash — inviare JSON al formattatore integrato di Python
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — inviare a jq per formattazione e filtraggio completi
# Solo formattare
cat api-response.json | jq .

# Estrarre un campo annidato
cat api-response.json | jq '.checks.database'

# Filtrare un array
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — inviare a un main.go minimale via stdin
# main.go: legge stdin, formatta, scrive 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.go
Nota:gofmt formatta il codice sorgente Go, non il JSON. Non inviare file JSON attraverso gofmt — produrrà un errore o un output irriconoscibile. Usa jq . o python3 -m json.tool per i file JSON.

Alternativa ad alte prestazioni — go-json

Per la grande maggioranza dei servizi Go, encoding/json è abbastanza veloce. Ma se la serializzazione JSON appare nel tuo profiler — comune nelle API REST ad alto throughput o nei servizi che emettono grosse righe di log strutturate a ogni richiesta — la libreria go-json è un sostituto diretto 3–5× più veloce con una superficie API identica.

bash — installare go-json
go get github.com/goccy/go-json
Go — sostituire encoding/json con go-json con una sola modifica all'import
package main

import (
	// Sostituisci questo:
	// "encoding/json"

	// Con questo — API identica, nessun'altra modifica al codice:
	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: "fattura.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))
	// L'output è identico a encoding/json — differisce solo la velocità
}

github.com/bytedance/sonic è la libreria JSON più veloce disponibile per Go, ma funziona solo su amd64 e arm64 (usa la compilazione JIT). Usa go-json quando hai bisogno di un sostituto portabile; ricorri a sonic quando sei su un'architettura nota e hai bisogno di ogni microsecondo in un percorso critico.

Lavorare con file JSON grandi

Sia json.MarshalIndent che json.Indent richiedono che l'intero payload sia in memoria. Per file superiori a 100 MB — esportazioni di dati, log di audit, payload di consumer Kafka — usa json.Decoder per fare lo streaming dell'input e processare i record uno alla volta.

Streaming di un grande array JSON con json.Decoder

Go — stream di un grande array JSON senza caricare in memoria
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("aprire %s: %w", path, err)
	}
	defer file.Close()

	dec := json.NewDecoder(file)

	// Leggere il '[' di apertura
	if _, err := dec.Token(); err != nil {
		return fmt.Errorf("leggere token di apertura: %w", err)
	}

	var processed int
	for dec.More() {
		var event AuditEvent
		if err := dec.Decode(&event); err != nil {
			return fmt.Errorf("decodificare evento %d: %w", processed, err)
		}
		// Processare un evento alla volta — utilizzo memoria costante
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("Processati %d eventi di audit
", processed)
	return nil
}

func main() {
	if err := processAuditLog("audit-2026-03.json"); err != nil {
		log.Fatalf("processare log di audit: %v", err)
	}
}

NDJSON — Un oggetto JSON per riga

Go — processare stream NDJSON riga per riga
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("aprire log: %v", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB per riga

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // saltare le righe malformate
		}
		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)
	}
}
Nota:Passa allo streaming quando il file supera i 100 MB o quando elabori stream non limitati (consumer Kafka, pipeline di log, letture di oggetti S3). Caricare un file JSON da 500 MB con os.ReadFilealloca quell'intero buffer sull'heap, genera pressione GC e può causare OOM nei container con memoria limitata.

Errori comuni

Ignorare il valore di errore restituito da MarshalIndent

Problema: Scartare l'errore con _ significa che un valore non serializzabile (un canale, una funzione, un numero complesso) produce silenziosamente nil come output o causa un panic più avanti quando chiami string(nil).

Soluzione: Controlla sempre l'errore. Se stai serializzando un tipo che dovrebbe sempre avere successo, un log.Fatalf con panic è meglio della perdita silenziosa di dati.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // stringa vuota se il marshal è fallito
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
Usare fmt.Println invece di os.Stdout.Write per output binario

Problema: fmt.Println(string(data)) aggiunge un carattere di nuova riga dopo il JSON, il che corrompe le pipeline che trattano l'output come byte grezzi — ad esempio quando si invia a jq o si scrive in un protocollo binario.

Soluzione: Usa os.Stdout.Write(data) per output binario pulito. Se hai bisogno di una nuova riga finale per la visualizzazione umana, aggiungila esplicitamente.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // aggiunge una nuova riga extra alla fine
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // nuova riga esplicita solo quando necessario
Dimenticare omitempty sui campi puntatore

Problema: Senza omitempty, un puntatore nil *string o *int viene serializzato come "field": null. Questo espone nomi di campi interni e può rompere i validatori di schema JSON rigidi sul lato del consumatore.

Soluzione: Aggiungi omitempty ai campi puntatore che vuoi assenti (non null) nell'output. Un *T nil con omitempty non produce alcuna chiave nel JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // appare come null quando nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // omesso quando nil
}
// {"event_id":"evt_3c7f"}
Usare map[string]interface{} invece di struct

Problema: Fare unmarshal in map[string]any perde le informazioni sui tipi, richiede type assertion manuali e produce un ordine di chiavi non deterministico — rendendo i diff JSON e i confronti di log più difficili.

Soluzione: Definisci una struct con i json tag appropriati. Le struct sono type-safe, serializzano più velocemente, producono un ordine di campi deterministico corrispondente alla definizione della struct e rendono il codice auto-documentante.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // type assertion necessaria, causa panic se tipo sbagliato
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 // tipizzato, sicuro, veloce

encoding/json vs alternative — Confronto rapido

Metodo
Output formattato
JSON valido
Tipi personalizzati
Streaming
Installazione richiesta
json.MarshalIndent
✓ via MarshalJSON
No (stdlib)
json.Indent
N/A (solo bytes)
No (stdlib)
json.Encoder
✗ (compatto)
✓ via MarshalJSON
No (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
Installazione di sistema

Usa json.MarshalIndent per qualsiasi caso in cui controlli la definizione della struct e hai bisogno di output formattato — file di configurazione, logging di debug, fixture di test e logging delle risposte API. Usa json.Indent quando hai già byte grezzi e hai solo bisogno di aggiungere spazi bianchi senza andata e ritorno attraverso i tipi Go. Passa a go-json o sonic solo dopo che il profiling conferma che la serializzazione JSON è un collo di bottiglia misurabile — per la maggior parte dei servizi, la libreria standard è più che sufficiente.

Domande frequenti

Come visualizzo JSON formattato in Go?

Chiama json.MarshalIndent(v, "", "\t") dal package encoding/json — il secondo argomento è un prefisso per riga (di solito vuoto) e il terzo è l'indentazione per livello. Passa "\t" per le tabulazioni o " " per due spazi. Non è necessaria alcuna libreria esterna; encoding/json è incluso nella libreria standard di Go.

Go
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	config := map[string]any{
		"service": "payments-api",
		"port":    8443,
		"region":  "eu-south-1",
	}
	data, err := json.MarshalIndent(config, "", "	")
	if err != nil {
		log.Fatalf("marshal: %v", err)
	}
	fmt.Println(string(data))
	// {
	// 	"port": 8443,
	// 	"region": "eu-south-1",
	// 	"service": "payments-api"
	// }
}

Qual è la differenza tra json.Marshal e json.MarshalIndent?

json.Marshal produce JSON compatto su una singola riga senza spazi bianchi — ideale per il trasferimento di rete dove ogni byte conta. json.MarshalIndent accetta due parametri extra (prefix e indent) e produce un output indentato e leggibile. Entrambe le funzioni accettano gli stessi tipi di valore e restituiscono ([]byte, error). L'unico costo di MarshalIndent è qualche byte in più nell'output e una quantità trascurabile di CPU extra per inserire gli spazi.

Go
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
// }

Come formatto un []byte JSON senza fare unmarshal della struct?

Usa json.Indent(&buf, src, "", "\t"). Questa funzione prende un []byte JSON esistente e scrive la versione indentata in un bytes.Buffer — senza definire una struct, senza type assertion, senza andata e ritorno attraverso i tipi Go. È l'opzione più rapida quando hai già byte JSON grezzi, ad esempio dal corpo di una risposta HTTP o da una colonna di database.

Go
import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

raw := []byte(`{"endpoint":"/api/v2/fatture","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/fatture",
// 	"page": 1,
// 	"per_page": 50,
// 	"total": 312
// }

Perché json.MarshalIndent restituisce un errore?

encoding/json restituisce un errore quando il valore non può essere rappresentato come JSON. Le cause più comuni sono: serializzare un canale, una funzione o un numero complesso (questi tipi non hanno equivalente JSON); una struct che implementa MarshalJSON() e restituisce un errore; o una map con chiavi che non sono string. Importante: serializzare una struct con un campo non esportato o un puntatore nil NON causa un errore — vengono semplicemente omessi.

Go
import (
	"encoding/json"
	"fmt"
)

// Questo restituirà un errore — i canali non sono serializzabili come JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Questo è corretto — i campi puntatore nil con omitempty vengono omessi silenziosamente
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 omesso, nessun errore

Come escludo un campo dall'output JSON in Go?

Esistono tre modi. Primo, usa il struct tag json:"-" — il campo è sempre escluso indipendentemente dal suo valore. Secondo, usa omitempty — il campo è escluso solo quando contiene il valore zero del suo tipo (puntatore nil, stringa vuota, 0, false). Terzo, i campi non esportati (in minuscolo) sono esclusi automaticamente da encoding/json senza bisogno di alcun tag.

Go
type PaymentMethod struct {
	ID           string  `json:"id"`
	Last4        string  `json:"last4"`
	ExpiryMonth  int     `json:"expiry_month"`
	ExpiryYear   int     `json:"expiry_year"`
	CVV          string  `json:"-"`                    // sempre escluso
	BillingName  string  `json:"billing_name,omitempty"` // escluso se vuoto
	internalRef  string                                 // non esportato — escluso automaticamente
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (vuoto) e internalRef non appaiono nell'output

Come gestisco time.Time nella serializzazione JSON?

encoding/json serializza time.Time nel formato RFC3339Nano per impostazione predefinita (es. "2026-03-10T14:22:00Z"), compatibile con ISO 8601. Se hai bisogno di un formato diverso — come interi Unix epoch per una API legacy, o una stringa solo data — implementa MarshalJSON() su un tipo wrapper che incorpora time.Time e restituisce il formato di cui hai bisogno.

Go
import (
	"encoding/json"
	"fmt"
	"time"
)

// Comportamento predefinito — RFC3339Nano, senza codice personalizzato
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

e := AuditEvent{
	Action:    "fattura.pagata",
	OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", "  ")
// {
//   "action": "fattura.pagata",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }

// Personalizzato: timestamp Unix come intero
type UnixTime struct{ time.Time }

func (u UnixTime) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("%d", u.Unix())), nil
}

Strumenti correlati

Disponibile anche in:PythonJavaScriptBash
JO
James OkaforSystems Engineer

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.

TM
Tobias MüllerRevisore tecnico

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.