JSON Formatter Go — Poradnik MarshalIndent()
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.
{"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.
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:
// 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
// }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.
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.
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ę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
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
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(), 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.
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"
// }[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.
Popularne opcje tagów struct:
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.
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.
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())
}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
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
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)
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.
// 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.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# 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")'
# 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.gogofmt 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.
go get github.com/goccy/go-json
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
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ę
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)
}
}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
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.
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))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.
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 potrzebnaProblem: 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.
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"}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.
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, szybkieencoding/json vs. alternatywy — szybkie porównanie
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.
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.
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.
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.
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łęduJak 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.
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ściuJak 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.
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
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.