JSON Formatter Go — Poradnik MarshalIndent()

·Systems Engineer·Sprawdzono przezTobias Müller·Opublikowano

Użyj darmowego Formater i Upiększacz JSON bezpośrednio w przeglądarce — bez instalacji.

Wypróbuj Formater i Upiększacz JSON online →

Kiedy pracuję nad mikroserwisem w Go i muszę zbadać odpowiedź API lub plik konfiguracyjny, skompresowany JSON jest pierwszą przeszkodą — pojedyncza linia z setkami zagnieżdżonych pól nic mi nie mówi na pierwszy rzut oka. Aby formatować JSON w Go, standardowa biblioteka daje ci wszystko, czego potrzebujesz: json.MarshalIndent jest wbudowany w encoding/json, dostarczany z każdą instalacją Go i nie wymaga żadnych zewnętrznych zależności. Ten przewodnik obejmuje pełny obraz: tagi struct, niestandardowe implementacje MarshalJSON(), json.Indent do ponownego formatowania surowych bajtów, strumieniowanie dużych plików za pomocą json.Decoder, kiedy sięgnąć po go-json dla ścieżek wysokiej przepustowości, oraz jednoliniowe polecenia CLI do szybkiego formatowania w terminalu. Wszystkie przykłady używają Go 1.21+.

  • json.MarshalIndent(v, "", "\t") to standardowa biblioteka — zero zależności, dostępna w każdej instalacji Go.
  • Tagi struct json:"field_name,omitempty" kontrolują klucze serializacji i pomijają pola o wartości zerowej w wyjściu.
  • Zaimplementuj MarshalJSON() na dowolnym typie, aby w pełni kontrolować jego reprezentację JSON.
  • json.Indent() ponownie formatuje już zserializowany []byte bez ponownego parsowania struktury — szybsze dla surowych bajtów.
  • Dla dużych plików (>100 MB): użyj json.Decoder z Token() do strumieniowania bez ładowania wszystkiego do pamięci.
  • go-json to zamiennik encoding/json, 3–5× szybszy dla API o wysokiej przepustowości.

Czym jest formatowanie JSON?

Formatowanie JSON — zwane też pretty-printingiem — przekształca kompaktowy, zminifikowany łańcuch JSON w czytelny dla człowieka układ ze spójnym wcięciem i podziałem na linie. Dane bazowe są identyczne; zmienia się tylko białe znaki. Kompaktowy JSON jest optymalny do transferu sieciowego, gdzie każdy bajt ma znaczenie; sformatowany JSON jest optymalny do debugowania, przeglądu kodu, inspekcji logów i tworzenia plików konfiguracyjnych. Pakiet encoding/json Go obsługuje oba tryby jednym wywołaniem funkcji — przełącz między kompaktowym a wciętym wyjściem wybierając między json.Marshal a json.MarshalIndent.

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

json.MarshalIndent() — Podejście standardowej biblioteki

json.MarshalIndent znajduje się w pakiecie encoding/json, który jest częścią standardowej biblioteki Go — nie potrzebujesz go get. Jego sygnatura to MarshalIndent(v any, prefix, indent string) ([]byte, error): łańcuch prefix jest dodawany przed każdą linią wyjściową (prawie zawsze pozostawiany pusty), a indent jest powtarzany raz na poziom zagnieżdżenia. Przekaż "\t" dla tabulatorów lub " " dla dwóch spacji.

Go — minimalny działający przykład
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"
// 	]
// }

Wybór między tabulatorami a spacjami jest głównie kwestią konwencji zespołu. Wiele projektów Go preferuje tabulatory, ponieważ gofmt (który formatuje kod źródłowy Go) używa tabulatorów. Dwie lub cztery spacje są powszechne, gdy JSON jest przeznaczony dla konsumenta JavaScript lub Python. Oto ta sama struktura z oboma stylami wcięć obok siebie:

Go — tabulatory vs. spacje
// Wcięcie tabulatorami — preferowane w natywnych narzędziach Go
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Wcięcie dwoma spacjami — powszechne dla API z konsumentami JS/Python
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Wcięcie czterema spacjami
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Uwaga:Używaj json.Marshal(v) gdy potrzebujesz kompaktowego wyjścia — ładunki sieciowe, wartości cache lub każda ścieżka, gdzie rozmiar binarny ma znaczenie. Przyjmuje ten sam argument wartości i ma tę samą semantykę błędów, ale produkuje JSON w jednej linii bez żadnych białych znaków.

Tagi struct — Kontrola nazw pól i Omitempty

Tagi struct Go to literały łańcuchowe umieszczane po deklaracjach pól, które mówiąencoding/jsonjak serializować każde pole. Są trzy kluczowe dyrektywy: json:"name" zmienia nazwę pola w wyjściu, omitempty pomija pole, gdy ma wartość zerową dla swojego typu, oraz json:"-" całkowicie wyklucza pole — przydatne dla haseł, wewnętrznych identyfikatorów lub pól, które nigdy nie powinny przekraczać granicy serwisu.

Go — tagi struct dla odpowiedzi API
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // pominięte jeśli pusty łańcuch
	AvatarURL   *string `json:"avatar_url,omitempty"`    // pominięte jeśli wskaźnik nil
	IsAdmin     bool    `json:"is_admin,omitempty"`      // pominięte jeśli false
	passwordHash string                                   // nieeksportowane — automatycznie wykluczone
}

// Użytkownik ze wszystkimi polami opcjonalnymi
full := UserProfile{
	ID: "usr_7b3c", Email: "ops@example.com",
	DisplayName: "Piotr Kowalski", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "ops@example.com",
//   "display_name": "Piotr Kowalski",
//   "is_admin": true
// }

// Użytkownik bez pól opcjonalnych — całkowicie pominięte
minimal := UserProfile{ID: "usr_2a91", Email: "dev@example.com"}
// {
//   "id": "usr_2a91",
//   "email": "dev@example.com"
// }

Tag json:"-" jest właściwym wyborem dla pól, które muszą być bezwarunkowo wykluczone niezależnie od ich wartości — zazwyczaj sekrety, wewnętrzne pola śledzące lub dane, które są poprawne w pamięci, ale nie mogą być serializowane do żadnego systemu zewnętrznego.

Go — wykluczanie wrażliwych pól
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // nigdy nie serializowane
	RefreshToken string `json:"-"`   // nigdy nie serializowane
}

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 i RefreshToken nigdy nie pojawiają się
Uwaga:Nieeksportowane (małe litery) pola struct są zawsze wykluczane przez encoding/json, niezależnie od tagów. Nie musisz dodawać json:"-" do nieeksportowanych pól — wykluczenie jest automatyczne i nie można go nadpisać.

Niestandardowy MarshalJSON() — Obsługa niestandardowych typów

Dowolny typ Go może zaimplementować interfejs json.Marshaler definiując metodę MarshalJSON() ([]byte, error). Gdy encoding/json napotka taki typ, wywołuje metodę zamiast domyślnego marshalingu opartego na refleksji. Jest to kanoniczny wzorzec Go dla typów domenowych wymagających określonej reprezentacji wire — wartości pieniężne, enumeracje statusów, niestandardowe formaty czasu lub dowolny typ, który przechowuje dane inaczej niż powinien być serializowany.

Niestandardowy typ — Money z konwersją groszy na złote

Go — niestandardowy MarshalJSON dla Money
package main

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

type Money struct {
	Amount   int64  // przechowywane w groszach, aby uniknąć dryftu zmiennoprzecinkowego
	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: "PLN"},
		Tax:      Money{Amount: 1592, Currency: "PLN"},
		Total:    Money{Amount: 21492, Currency: "PLN"},
	}
	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": "PLN", "display": "PLN 199.00" },
//   "tax":      { "amount": 15.92, "currency": "PLN", "display": "PLN 15.92" },
//   "total":    { "amount": 214.92, "currency": "PLN", "display": "PLN 214.92" }
// }

Enumeracja statusów — reprezentacja łańcuchowa

Go — niestandardowy MarshalJSON dla enumeracji statusu
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"
// }
Uwaga:Zawsze implementuj zarówno MarshalJSON(), jak i UnmarshalJSON() razem. Jeśli implementujesz tylko marshaling, podróż w obie strony przez JSON (serializacja → przechowywanie → deserializacja) cicho straci strukturę lub zwróci zły typ. Para tworzy kontrakt, że typ może przeżyć podróż przez JSON.

UUID — serializacja jako łańcuch

Standardowa biblioteka Go nie ma typu UUID. Najczęstszym wyborem jest github.com/google/uuid, który już implementuje MarshalJSON() i serializuje jako cytowany łańcuch RFC 4122. Jeśli używasz surowego [16]byte lub niestandardowego typu ID, zaimplementuj interfejs samodzielnie, aby uniknąć zakodowanych base64 binarnych bloków w wyjściu JSON.

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

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // serializuje jako "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"
// }
Uwaga:Jeśli przechowujesz UUID jako [16]byte bez niestandardowego typu, encoding/json zakoduje je jako łańcuch base64 — np. "VQ6EAOKbQdSnFkRmVUQAAA==". Zawsze używaj właściwego typu UUID lub implementuj MarshalJSON() aby emitować kanoniczny format z łącznikami.

Tabela parametrów json.MarshalIndent()

Sygnatura funkcji to func MarshalIndent(v any, prefix, indent string) ([]byte, error). Zarówno prefix, jak i indent to literały łańcuchowe — nie ma numerycznego skrótu jak w Pythonie indent=4.

Parametr
Type
Opis
v
any
Wartość do zmarshalowania — struct, mapa, slice lub typ prymitywny
prefix
string
Ciąg dodawany przed każdą linią wyjściową (zazwyczaj "")
indent
string
Ciąg używany dla każdego poziomu wcięcia ("\t" lub " ")

Popularne opcje tagów struct:

Tag
Działanie
json:"name"
Zmień nazwę pola na name w wyjściu JSON
json:"name,omitempty"
Zmień nazwę + pomiń jeśli wartość zerowa (nil, "", 0, false)
json:"-"
Zawsze wyklucz to pole z wyjścia JSON
json:",string"
Koduj liczbę lub bool jako wartość string JSON

json.Indent() — Ponowne formatowanie istniejących bajtów JSON

Gdy masz już []byte JSON — powiedzmy z ciała odpowiedzi HTTP, kolumny jsonb PostgreSQL, lub pliku odczytanego za pomocą os.ReadFile — nie musisz definiować struktury i unmarshalu przed ładnym drukowaniem. json.Indent bezpośrednio ponownie formatuje surowe bajty, zapisując wcięte wyjście do bytes.Buffer.

Go — json.Indent na surowych bajtach
package main

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

func main() {
	// Symulacja surowego ładunku JSON z upstream serwisu
	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
// }

Wzorzec, którego często używam w mikroserwisach, to wywoływanie json.Indent przed zapisem do strukturyzowanych logów — dodaje to pomijalny narzut i sprawia, że wpisy dziennika są o wiele łatwiejsze do odczytania podczas incydentu. Funkcja jest szczególnie przydatna do logowania odpowiedzi HTTP, ładnego drukowania przechowywanych łańcuchów JSON i potoków format-on-read, gdzie definicja struktury jest niedostępna.

Go — json.Indent do logowania debugowego
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Ciało nie jest prawidłowym JSON — zaloguj surowo
		logger.Debug("upstream response", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}
Ostrzeżenie:json.Indent NIE waliduje JSON w pełni poza tym, co jest strukturalnie potrzebne do wstawienia białych znaków. Dla pełnej walidacji składni wywołaj najpierw json.Valid(data) i obsłuż przypadek false przed próbą wcięcia.

Formatowanie JSON z pliku i odpowiedzi HTTP

Dwa z najczęstszych scenariuszy w serwisach Go to formatowanie JSON odczytanego z pliku na dysku (pliki konfiguracyjne, dane fixture, ziarna migracji) oraz ładne drukowanie ciał odpowiedzi HTTP do logowania debugowego lub asercji testowych. Oba następują po tym samym wzorcu: odczytaj bajty, wywołaj json.Indent lub unmarshalu, a następnie json.MarshalIndent, zapisz z powrotem lub zaloguj.

Odczyt pliku → Formatowanie → Zapis z powrotem

Go — formatowanie pliku JSON w miejscu
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/database.json"); err != nil {
		log.Fatalf("format config: %v", err)
	}
	fmt.Println("config/database.json sformatowany pomyślnie")
}

Odpowiedź HTTP → Dekodowanie → Ładne drukowanie do logowania debugowego

Go — formatowanie odpowiedzi HTTP do logowania debugowego
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("Kontrola zdrowia (%d):
%s
", resp.StatusCode, string(pretty))
}
// Kontrola zdrowia (200):
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "checks": {
//     "database": "ok",
//     "cache": "ok",
//     "queue": "degraded"
//   },
//   "uptime_seconds": 172800
// }

Surowe ciało odpowiedzi → json.Indent (bez struktury)

Go — formatowanie surowego ciała HTTP za pomocą 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("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())
}

Ładne drukowanie JSON z odpowiedzi HTTP w Go

Dwa powyższe podejścia obejmują najczęstsze przypadki: dekodowanie do typowanej struktury i wywołanie json.MarshalIndent (najlepsze gdy musisz walidować lub sprawdzać określone pola) lub odczytanie surowych bajtów ciała za pomocą io.ReadAll i wywołanie json.Indent bezpośrednio (najlepsze do szybkiego logowania debugowego bez dostępnej definicji struktury). Podejście z surowymi bajtami jest prostsze, ale nie daje bezpieczeństwa typów Go ani dostępu do pól — jest to czysta transformacja wyświetlania. Oba podejścia poprawnie obsługują duże ciała odpowiedzi, o ile całe ciało mieści się w pamięci.

Go — dwa wzorce obok siebie
// Wzorzec A: typowane dekodowanie → MarshalIndent
// Użyj, gdy musisz sprawdzić lub walidować określone pola
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Wzorzec B: surowe bajty → json.Indent
// Użyj do szybkiego logowania debugowego — bez definicji struktury
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Formatowanie JSON z wiersza poleceń w projektach Go

Czasami musisz sformatować ładunek JSON bezpośrednio w terminalu bez pisania programu w Go. To są jednoliniowce, które mam w pamięci mięśniowej podczas tworzenia kodu i reagowania na incydenty.

bash — przekazanie JSON do wbudowanego formatera Pythona
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — przekazanie do jq dla pełnego formatowania i filtrowania
# Tylko formatowanie
cat api-response.json | jq .

# Wyodrębnij zagnieżdżone pole
cat api-response.json | jq '.checks.database'

# Filtruj tablicę
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — przekazanie do prostego Go main.go przez stdin
# main.go: czyta stdin, formatuje, zapisuje 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
Uwaga:gofmt formatuje kod źródłowy Go, nie JSON. Nie przekazuj plików JSON przez gofmt — spowoduje to błąd lub nierozpoznawalne wyjście. Używaj jq . lub python3 -m json.tool dla plików JSON.

Wysokowydajna alternatywa — go-json

Dla zdecydowanej większości serwisów Go encoding/json jest wystarczająco szybki. Ale jeśli marshaling JSON pojawia się w profilerze — co jest powszechne w REST API o wysokiej przepustowości lub serwisach emitujących duże linie logów strukturyzowanych przy każdym żądaniu — biblioteka go-json jest zamiennikiem, który jest 3–5× szybszy przy identycznym interfejsie API.

bash — instalacja go-json
go get github.com/goccy/go-json
Go — zamiana encoding/json na go-json jedną zmianą importu
package main

import (
	// Zamień to:
	// "encoding/json"

	// Na to — identyczne API, żadnych innych zmian kodu:
	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: "invoice.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))
	// Wyjście jest identyczne z encoding/json — różni się tylko szybkość
}

github.com/bytedance/sonic to najszybsza dostępna biblioteka JSON dla Go, ale działa tylko na amd64 i arm64 (używa kompilacji JIT). Używaj go-json gdy potrzebujesz przenośnego zamiennika; sięgaj po sonic gdy jesteś na znanej architekturze i potrzebujesz każdej mikrosekundy w gorącej ścieżce.

Praca z dużymi plikami JSON

Zarówno json.MarshalIndent, jak i json.Indent wymagają, aby cały ładunek był w pamięci. Dla plików powyżej 100 MB — eksporty danych, dzienniki audytowe, ładunki konsumentów Kafka — używaj json.Decoder do strumieniowania wejścia i przetwarzania rekordów jeden po drugim.

Strumieniowanie dużej tablicy JSON za pomocą json.Decoder

Go — strumieniowanie dużej tablicy JSON bez ładowania do pamięci
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)

	// Odczyt otwierającego '['
	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)
		}
		// Przetwarzaj jedno zdarzenie na raz — stałe użycie pamięci
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("Przetworzono %d zdarzeń audytowych
", processed)
	return nil
}

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

NDJSON — jeden obiekt JSON na linię

Go — przetwarzanie strumienia dziennika NDJSON linia po linii
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 na linię

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // pomiń zniekształcone linie
		}
		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)
	}
}
Uwaga:Przejdź na strumieniowanie, gdy plik przekracza 100 MB lub gdy przetwarzasz nieograniczone strumienie (konsumenci Kafka, potoki logów, odczyty obiektów S3). Załadowanie 500 MB pliku JSON za pomocą os.ReadFile przydzieli cały bufor na stercie, wywoła presję GC i może spowodować OOM na kontenerach z ograniczoną pamięcią.

Częste błędy

Ignorowanie wartości błędu z MarshalIndent

Problem: Odrzucenie błędu za pomocą _ oznacza, że nieserializowalna wartość (kanał, funkcja, liczba zespolona) cicho produkuje wyjście nil lub powoduje panikę downstream podczas wywołania string(nil).

Rozwiązanie: Zawsze sprawdzaj błąd. Jeśli marshalizujesz typ, który zawsze powinien się udawać, panikowne log.Fatalf jest lepsze niż ciche utracenie danych.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // pusty łańcuch jeśli marshal się nie powiódł
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
Używanie fmt.Println zamiast os.Stdout.Write dla binarnego wyjścia

Problem: fmt.Println(string(data)) dodaje znak nowej linii po JSON, co psuje potoki traktujące wyjście jako surowe bajty — na przykład przy przekazywaniu do jq lub zapisie do protokołu binarnego.

Rozwiązanie: Używaj os.Stdout.Write(data) dla czystego binarnego wyjścia. Jeśli potrzebujesz końcowej nowej linii do ludzkiego wyświetlania, dodaj ją jawnie.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // dodaje dodatkową nową linię na końcu
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // jawna nowa linia tylko gdy potrzebna
Zapomnienie o omitempty dla pól wskaźnikowych

Problem: Bez omitempty wskaźnik nil *string lub *int serializuje się jako "field": null. Ujawnia to wewnętrzne nazwy pól i może uszkodzić rygorystyczne walidatory schematu JSON po stronie konsumenta.

Rozwiązanie: Dodaj omitempty do pól wskaźnikowych, które mają być nieobecne (nie null) w wyjściu. Wskaźnik nil *T z omitempty w ogóle nie produkuje klucza w JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // pojawia się jako null gdy nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // pominięte gdy nil
}
// {"event_id":"evt_3c7f"}
Używanie map[string]interface{} zamiast struktur

Problem: Unmarshaling do map[string]any traci informacje o typach, wymaga ręcznych asercji typów i produkuje niedeterministyczną kolejność kluczy — utrudniając porównania JSON diff i dzienników.

Rozwiązanie: Zdefiniuj strukturę z odpowiednimi tagami json. Struktury są bezpieczne typologicznie, szybciej marshalują, produkują deterministyczną kolejność pól zgodną z definicją struktury i sprawią, że kod dokumentuje się sam.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // asercja typu wymagana, panikuje przy złym typie
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 // typowane, bezpieczne, szybkie

encoding/json vs. alternatywy — szybkie porównanie

Metoda
Czytelne wyjście
Prawidłowy JSON
Niestandardowe typy
Strumieniowanie
Instalacja
json.MarshalIndent
✓ przez MarshalJSON
Nie (stdlib)
json.Indent
N/A (tylko bajty)
Nie (stdlib)
json.Encoder
✗ (kompaktowy)
✓ przez MarshalJSON
Nie (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
Instalacja systemowa

Używaj json.MarshalIndent dla każdego przypadku, gdy kontrolujesz definicję struktury i potrzebujesz sformatowanego wyjścia — pliki konfiguracyjne, logowanie debugowe, dane testowe i logowanie odpowiedzi API. Używaj json.Indent gdy masz już surowe bajty i chcesz tylko dodać białe znaki bez podróży przez typy Go. Przejdź na go-json lub sonic dopiero po tym, jak profilowanie potwierdzi, że marshaling JSON jest wymiernym wąskim gardłem — dla większości serwisów standardowa biblioteka jest więcej niż wystarczająca.

Często zadawane pytania

Jak wydrukować JSON czytelnie w Go?

Wywołaj json.MarshalIndent(v, "", "\t") z pakietu encoding/json — drugi argument to prefiks wiersza (zazwyczaj pusty), a trzeci to wcięcie na poziom. Przekaż "\t" dla tabulatorów lub " " dla dwóch spacji. Nie jest wymagana żadna zewnętrzna biblioteka; encoding/json jest częścią standardowej biblioteki Go.

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

Jaka jest różnica między json.Marshal a json.MarshalIndent?

json.Marshal produkuje kompaktowy JSON w jednej linii bez białych znaków — idealny do transferu sieciowego, gdzie każdy bajt ma znaczenie. json.MarshalIndent przyjmuje dwa dodatkowe parametry łańcuchowe (prefix i indent) i produkuje wcięte, czytelne dla człowieka wyjście. Obie funkcje przyjmują te same typy wartości i zwracają ([]byte, error). Jedynym kosztem MarshalIndent jest nieco więcej bajtów wyjściowych i pomijalnie mała dodatkowa praca CPU przy wstawianiu białych znaków.

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

Jak sformatować JSON []byte bez unmarshaling struktury?

Użyj json.Indent(&buf, src, "", "\t"). Funkcja ta przyjmuje istniejący []byte JSON i zapisuje wciętą wersję do bytes.Buffer — nie potrzebujesz definicji struktury, asercji typów ani podróży w obie strony przez typy Go. To najszybsza opcja, gdy masz już surowe bajty JSON, na przykład z ciała odpowiedzi HTTP lub kolumny bazy danych.

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

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

Dlaczego json.MarshalIndent zwraca błąd?

encoding/json zwraca błąd, gdy wartość nie może być reprezentowana jako JSON. Najczęstsze przyczyny to: marshaling kanału, funkcji lub liczby zespolonej (nie mają odpowiednika w JSON); struct implementujący MarshalJSON(), który zwraca błąd; lub mapa z kluczami niebędącymi łańcuchami. Co ważne, marshaling struktury z polem nieeksportowanym lub wskaźnikiem nil NIE powoduje błędu — te pola są po prostu pomijane.

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

// To zwróci błąd — kanały nie są serializowalne do JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// To jest w porządku — pola wskaźnikowe nil z omitempty są cicho pomijane
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 pominięty, brak błędu

Jak wykluczyć pole z wyjścia JSON w Go?

Są trzy sposoby. Po pierwsze, użyj tagu struct json:"-" — pole jest zawsze wykluczane niezależnie od jego wartości. Po drugie, użyj omitempty — pole jest wykluczane tylko wtedy, gdy ma wartość zerową dla swojego typu (wskaźnik nil, pusty łańcuch, 0, false). Po trzecie, nieeksportowane (małe litery) pola są automatycznie wykluczane przez encoding/json bez konieczności stosowania tagów.

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:"-"`                    // zawsze wykluczone
	BillingName  string  `json:"billing_name,omitempty"` // wykluczone jeśli puste
	internalRef  string                                 // nieeksportowane — automatycznie wykluczone
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (puste) i internalRef nie pojawiają się w wyjściu

Jak obsługiwać time.Time przy marshalingu JSON?

encoding/json domyślnie marshaluje time.Time do formatu RFC3339Nano (np. "2026-03-10T14:22:00Z"), który jest zgodny z ISO 8601. Jeśli potrzebujesz innego formatu — na przykład liczb całkowitych epoki Unix dla starszego API lub niestandardowego łańcucha daty — zaimplementuj MarshalJSON() na typie opakowującym, który osadza time.Time i zwraca potrzebny format.

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

// Domyślne zachowanie — RFC3339Nano, bez niestandardowego kodu
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

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

// Niestandardowy: znacznik czasu Unix (liczba całkowita)
type UnixTime struct{ time.Time }

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

Powiązane narzędzia

Dostępne również w: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üllerRecenzent techniczny

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.