JSON Formatter Go — Посібник MarshalIndent()

·Systems Engineer·ПеревіреноTobias Müller·Опубліковано

Використовуйте безкоштовний JSON Formatter & Beautifier прямо в браузері — без встановлення.

Спробувати JSON Formatter & Beautifier онлайн →

Коли я працюю над Go-мікросервісом і потрібно дослідити відповідь API або файл конфігурації, компактний JSON — перша перешкода: один рядок із сотнями вкладених полів майже нічого не говорить з першого погляду. Щоб форматувати JSON у Go, стандартна бібліотека надає все необхідне: json.MarshalIndent вбудований у encoding/json, постачається з кожною інсталяцією Go і не потребує сторонніх залежностей. Цей посібник охоплює повну картину: struct tags, кастомні MarshalJSON()-реалізації, json.Indent для переформатування сирих байт, стрімінг великих файлів через json.Decoder, і коли варто переключитися на go-json для високонавантажених шляхів — плюс CLI-однорядники для швидкого форматування у терміналі. Усі приклади написані для Go 1.21+.

  • json.MarshalIndent(v, "", "\t") — стандартна бібліотека: нуль залежностей, входить до кожної інсталяції Go.
  • Struct tags json:"field_name,omitempty" керують ключами серіалізації та пропускають поля з нульовим значенням.
  • Реалізуйте MarshalJSON() на будь-якому типі для повного контролю над його JSON-представленням.
  • json.Indent() переформатовує вже серіалізований []byte без повторного парсингу структури — швидше для сирих байт.
  • Для великих файлів (>100 МБ): використовуйте json.Decoder з Token() для стрімінгу без завантаження всього в пам'ять.
  • go-json — drop-in замінник у 3–5× швидше encoding/json для високонавантажених API.

Що таке форматування JSON?

Форматування JSON — також зване pretty-printing — перетворює компактний мінімізований JSON-рядок на читабельну структуру з послідовними відступами та переносами рядків. Базові дані ідентичні; змінюються лише пробільні символи. Компактний JSON оптимальний для передачі по мережі, де важливий кожен байт; форматований JSON — для налагодження, код-рев'ю, інспекції логів і написання конфігураційних файлів. Пакет encoding/json у Go підтримує обидва режими одним викликом функції — перемикання між компактним і форматованим виводом досягається вибором між json.Marshal і json.MarshalIndent.

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

json.MarshalIndent() — підхід стандартної бібліотеки

json.MarshalIndent знаходиться в пакеті encoding/json, що входить до стандартної бібліотеки Go — жодного go get не потрібно. Сигнатура: MarshalIndent(v any, prefix, indent string) ([]byte, error): рядок prefix додається до кожного рядка виводу (майже завжди залишається порожнім), а indent повторюється один раз на кожний рівень вкладеності. Передавайте "\t" для табів або " " для двох пробілів.

Go — мінімальний робочий приклад
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"
// 	]
// }

Вибір між табами та пробілами — переважно командна домовленість. Багато Go-проектів надають перевагу табам, тому що gofmt (форматувальник Go-коду) використовує таби. Дво- або чотирипробільні відступи поширені, коли JSON призначений для JavaScript або Python-споживачів. Ось та сама структура з обома стилями відступів поруч:

Go — таби vs пробіли
// Таби — перевага в Go-нативному інструментарії
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Два пробіли — популярно для API з JS/Python-споживачами
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Чотири пробіли
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Примітка:Використовуйте json.Marshal(v) коли потрібен компактний вивід — мережеві payload-и, значення кешу або будь-який шлях, де розмір даних має значення. Функція приймає той самий аргумент-значення і має ту саму семантику помилок, але створює однорядковий JSON без пробілів.

Struct Tags — керування іменами полів та omitempty

Struct tags у Go — рядкові літерали після оголошень полів, що повідомляютьencoding/jsonяк серіалізувати кожне поле. Три ключові директиви: json:"name" перейменовує поле у виводі, omitempty пропускає поле з нульовим значенням, і json:"-" виключає поле повністю — корисно для паролів, внутрішніх ідентифікаторів або полів, що не повинні покидати межу сервісу.

Go — struct tags для API-відповіді
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // пропустити якщо порожній рядок
	AvatarURL   *string `json:"avatar_url,omitempty"`    // пропустити якщо nil-вказівник
	IsAdmin     bool    `json:"is_admin,omitempty"`      // пропустити якщо false
	passwordHash string                                   // неекспортоване — авто-виключення
}

// Користувач з усіма заповненими опціональними полями
full := UserProfile{
	ID: "usr_7b3c", Email: "o.kovalenko@poshta.ua",
	DisplayName: "Олексій Коваленко", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "o.kovalenko@poshta.ua",
//   "display_name": "Олексій Коваленко",
//   "is_admin": true
// }

// Користувач без опціональних полів — вони повністю пропускаються
minimal := UserProfile{ID: "usr_2a91", Email: "m.shevchenko@poshta.ua"}
// {
//   "id": "usr_2a91",
//   "email": "m.shevchenko@poshta.ua"
// }

Тег json:"-" — правильний вибір для полів, що мають безумовно виключатися незалежно від значення: зазвичай секрети, внутрішні tracking-поля або дані, правильні в пам'яті, але не призначені для серіалізації у зовнішні системи.

Go — виключення чутливих полів
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // ніколи не серіалізується
	RefreshToken string `json:"-"`   // ніколи не серіалізується
}

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 та RefreshToken не з'являться ніколи
Примітка:Неекспортовані (малі) поля структур завжди виключаються encoding/json незалежно від тегів. Додавати json:"-" до неекспортованих полів не потрібно — виключення автоматичне і не може бути перевизначено.

Кастомний MarshalJSON() — робота з нестандартними типами

Будь-який Go-тип може реалізувати інтерфейс json.Marshaler, визначивши метод MarshalJSON() ([]byte, error). Коли encoding/json зустрічає такий тип, він викликає цей метод замість стандартного рефлексивного маршалінгу. Це канонічний Go-патерн для доменних типів, яким потрібне специфічне wire-представлення — грошові суми, статус-еніми, кастомні формати часу або будь-який тип, що зберігає дані інакше, ніж вони мають серіалізуватися.

Власний тип — Money з конвертацією копійок у гривні

Go — кастомний MarshalJSON для Money
package main

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

type Money struct {
	Amount   int64  // зберігається в копійках, щоб уникнути помилок з плаваючою точкою
	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: "UAH"},
		Tax:      Money{Amount: 1592, Currency: "UAH"},
		Total:    Money{Amount: 21492, Currency: "UAH"},
	}
	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": "UAH", "display": "UAH 199.00" },
//   "tax":      { "amount": 15.92, "currency": "UAH", "display": "UAH 15.92" },
//   "total":    { "amount": 214.92, "currency": "UAH", "display": "UAH 214.92" }
// }

Status Enum — рядкове представлення

Go — кастомний MarshalJSON для статус-еніму
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() та UnmarshalJSON() разом. Якщо реалізувати лише маршалінг, round-trip типу через JSON (серіалізація → збереження → десеріалізація) мовчки втратить структуру або поверне невірний тип. Пара утворює контракт того, що тип переживе JSON-round-trip.

UUID — серіалізація як рядок

У стандартній бібліотеці Go немає типу UUID. Найпоширеніший вибір — github.com/google/uuid, який вже реалізує MarshalJSON() і серіалізується як quoted RFC-4122-рядок. При використанні сирого [16]byte або кастомного ID-типу варто самостійно реалізувати інтерфейс, щоб уникнути base64-кодованих бінарних блобів у JSON-виводі.

Go 1.21+ — серіалізація UUID
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // серіалізується як "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"
// }
Примітка:Якщо UUID зберігаються як [16]byte без кастомного типу, encoding/json кодує їх як base64-рядок — наприклад "VQ6EAOKbQdSnFkRmVUQAAA==". Завжди використовуйте повноцінний UUID-тип або реалізуйте MarshalJSON() для виводу канонічного рядка з дефісами.

Довідник параметрів json.MarshalIndent()

Сигнатура функції: func MarshalIndent(v any, prefix, indent string) ([]byte, error). І prefix, і indent — рядкові літерали; числового скорочення як у Python's indent=4 тут немає.

Параметр
Тип
Опис
v
any
Значення для маршалінгу — struct, map, slice або примітив
prefix
string
Рядок, що додається до кожного рядка виводу (зазвичай "")
indent
string
Рядок для кожного рівня відступу ("\t" або " ")

Поширені опції struct tags:

Тег
Ефект
json:"name"
Перейменовує поле у JSON-виводі
json:"name,omitempty"
Перейменування + пропуск при нульовому значенні (nil, "", 0, false)
json:"-"
Завжди виключає це поле з JSON-виводу
json:",string"
Кодує число або bool як рядкове JSON-значення

json.Indent() — переформатування готових JSON-байт

Якщо вже є []byte з JSON — наприклад з тіла HTTP-відповіді, стовпця Postgres jsonb або файлу, зчитаного через os.ReadFile — не потрібно визначати структуру і виконувати анмаршалінг перед форматуванням. json.Indent безпосередньо переформатовує сирі байти, записуючи форматований вивід у bytes.Buffer.

Go — json.Indent для сирих байт
package main

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

func main() {
	// Симуляція сирого JSON-payload від вищестоящого сервісу
	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
// }

Патерн, який я регулярно використовую в мікросервісах: викликати json.Indent перед записом у структуровані логи — це додає зневажливо малі накладні витрати і робить записи в логах значно зручнішими для читання під час інциденту. Функція особливо корисна для логування HTTP-відповідей, pretty-printing збережених JSON-рядків і pipeline-ів з форматуванням при читанні, коли визначення структури недоступне.

Go — json.Indent для debug-логування
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Body не є валідним JSON — логуємо сирим
		logger.Debug("upstream response", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}
Попередження:json.Indent НЕ виконує повну валідацію JSON — лише те, що структурно необхідно для вставки пробілів. Для повної перевірки синтаксису спочатку викличте json.Valid(data) і обробіть випадок false перед спробою форматування.

Форматування JSON з файлу та HTTP-відповіді

Два найпоширеніші реальні сценарії в Go-сервісах — форматування JSON, зчитаного з файлу на диску (конфіги, fixture-дані, seed-и міграцій), і pretty-printing тіла HTTP-відповідей для debug-логування або тестових тверджень. Обидва слідують одному патерну: зчитати байти, викликати json.Indent або анмаршалити і потім json.MarshalIndent, записати назад або логувати.

Читаємо файл → форматуємо → записуємо назад

Go — форматування JSON-файлу на місці
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/baza-danykh.json"); err != nil {
		log.Fatalf("format config: %v", err)
	}
	fmt.Println("config/baza-danykh.json успішно відформатовано")
}

HTTP-відповідь → Decode → Pretty-Print для debug-логування

Go — форматування HTTP-відповіді для 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("decode health response: %v", err)
	}

	pretty, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("marshal health response: %v", err)
	}
	fmt.Printf("Health check (%d):
%s
", resp.StatusCode, string(pretty))
}
// Health check (200):
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "checks": {
//     "database": "ok",
//     "cache": "ok",
//     "queue": "degraded"
//   },
//   "uptime_seconds": 172800
// }

Сире тіло відповіді → json.Indent (структура не потрібна)

Go — форматування сирого HTTP-тіла через 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())
}

Форматований вивід JSON з HTTP-відповіді у Go

Два описані вище підходи покривають найпоширеніші випадки: декодування в типізовану структуру з подальшим викликом json.MarshalIndent (найкращий вибір, коли потрібно валідувати або інспектувати поля), або читання сирих байт тіла через io.ReadAll і прямий виклик json.Indent (найкращий вибір для швидкого debug-логування без визначення структури). Підхід з сирими байтами простіший, але не дає типобезпеки Go і доступу до полів — це виключно візуальна трансформація. Обидва підходи коректно обробляють великі тіла відповідей, поки все тіло вміщується в пам'ять.

Go — два патерни поруч
// Патерн A: типізований decode → MarshalIndent
// Використовувати, коли потрібно інспектувати або валідувати конкретні поля
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Патерн B: сирі байти → json.Indent
// Для швидкого debug-логування — визначення структури не потрібне
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Форматування JSON у командному рядку в Go-проектах

Іноді потрібно відформатувати JSON-payload прямо в терміналі, не пишучи Go-програму. Ось однорядники, що відклалися в м'язовій пам'яті під час розробки та розбору інцидентів.

bash — передати JSON у вбудований форматувальник Python
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — передати в jq для форматування та фільтрації
# Лише форматування
cat api-response.json | jq .

# Витягти вкладене поле
cat api-response.json | jq '.checks.database'

# Фільтрувати масив
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — передати в мінімальний Go main.go через stdin
# main.go: читає stdin, форматує, пише 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
Примітка:gofmt форматує Go-вихідний код, а не JSON. Не передавайте JSON-файли через gofmt — це або викличе помилку, або видасть нечитабельний вивід. Для JSON-файлів використовуйте jq . або python3 -m json.tool.

Високопродуктивна альтернатива — go-json

Для переважної більшості Go-сервісів encoding/json достатньо швидкий. Але якщо JSON-маршалінг з'являється в профайлері — що характерно для високонавантажених REST API або сервісів, що генерують великі структуровані рядки логу на кожний запит — бібліотека go-json є drop-in замінником, що в 3–5× швидше при ідентичному API.

bash — встановити go-json
go get github.com/goccy/go-json
Go — замінити encoding/json на go-json однією зміною імпорту
package main

import (
	// Замініть це:
	// "encoding/json"

	// На це — ідентичний API, жодних інших змін у коді:
	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: "rakhunok.zavantazhyty", 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))
	// Вивід ідентичний encoding/json — відрізняється лише швидкість
}

github.com/bytedance/sonic — найшвидша Go-бібліотека для роботи з JSON, але працює лише на amd64 і arm64 (використовує JIT-компіляцію). Використовуйте go-json коли потрібна портативна заміна; переходьте на sonic коли архітектура відома і кожна мікросекунда важлива на гарячому шляху.

Робота з великими JSON-файлами

json.MarshalIndent і json.Indent потребують, щоб увесь payload знаходився в пам'яті. Для файлів понад 100 МБ — вивантаження даних, audit-логи, payload-и Kafka-консумерів — використовуйте json.Decoder для потокової обробки вхідних даних і обробки записів по одному за раз.

Стрімінг великого JSON-масиву з json.Decoder

Go — стрімінг великого JSON-масиву без завантаження в пам'ять
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)

	// Читаємо відкриваючий '['
	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)
		}
		// Обробляємо одну подію за раз — постійне споживання пам'яті
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("Оброблено %d audit-подій
", processed)
	return nil
}

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

NDJSON — один JSON-об'єкт на рядок

Go — порядкова обробка NDJSON-лог-стріму
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 МБ на рядок

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // пропускаємо некоректні рядки
		}
		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)
	}
}
Примітка:Переходьте на стрімінг, коли файл перевищує 100 МБ або при обробці необмежених потоків (Kafka-консумери, лог-pipeline-и, читання S3-об'єктів). Завантаження 500-мегабайтного JSON-файлу через os.ReadFile виділить увесь буфер у купі, створить тиск на GC і може викликати OOM на контейнерах з обмеженою пам'яттю.

Поширені помилки

Ігнорування повернення помилки від MarshalIndent

Проблема: Відкидання помилки через _ означає, що несеріалізоване значення (канал, функція, комплексне число) мовчки створить nil-вивід або викличе паніку нижче по коду при виклику string(nil).

Рішення: Завжди перевіряйте помилку. Якщо серіалізуєте тип, що має завжди успішно серіалізуватися, паніка через log.Fatalf краща за мовчазну втрату даних.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // порожній рядок якщо marshal провалився
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
Використання fmt.Println замість os.Stdout.Write для бінарного виводу

Проблема: fmt.Println(string(data)) додає символ переносу рядка після JSON, що пошкоджує pipeline-и, які обробляють вивід як сирі байти — наприклад, при передачі в jq або записі в бінарний протокол.

Рішення: Використовуйте os.Stdout.Write(data) для бінарно-чистого виводу. Якщо потрібен завершальний перенос рядка для відображення людині, додайте його явно.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // додає зайвий перенос рядка в кінці
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // явний перенос рядка лише коли потрібен
Забутий omitempty для полів-вказівників

Проблема: Без omitempty nil-*string або *int-вказівник серіалізується як "field": null. Це розкриває внутрішні імена полів і може зламати суворі JSON-schema-валідатори на стороні споживача.

Рішення: Додайте omitempty до полів-вказівників, які мають бути відсутні (не null) у виводі. nil *T з omitempty взагалі не створює ключа в JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // з'являється як null якщо nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // пропускається якщо nil
}
// {"event_id":"evt_3c7f"}
Використання map[string]interface{} замість структур

Проблема: Анмаршалінг у map[string]any втрачає інформацію про типи, вимагає ручних type-assertions і створює недетермінований порядок ключів — що ускладнює JSON-дифференціювання та порівняння логів.

Рішення: Визначте структуру з правильними json-тегами. Структури типобезпечні, маршалються швидше, створюють детермінований порядок полів відповідно до визначення структури і роблять код самодокументованим.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // type-assertion обов'язковий, паніка при неправильному типі
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 // типізовано, безпечно, швидко

encoding/json vs альтернативи — швидке порівняння

Метод
Форматований вивід
Валідний JSON
Кастомні типи
Стрімінг
Потрібна установка
json.MarshalIndent
✓ via MarshalJSON
No (stdlib)
json.Indent
N/A (bytes only)
No (stdlib)
json.Encoder
✗ (compact)
✓ via MarshalJSON
No (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
System install

Використовуйте json.MarshalIndent коли контролюєте визначення структури і потрібен форматований вивід — конфігураційні файли, debug-логування, test-fixture-и та логування API-відповідей. Використовуйте json.Indent коли вже є сирі байти і потрібно просто додати пробіли без проходу через Go-типи. Переходьте на go-json або sonic лише після того, як профілювання підтвердило, що JSON-маршалінг є вимірним вузьким місцем — для більшості сервісів стандартної бібліотеки більш ніж достатньо.

Часті запитання

Як форматувати JSON у Go?

Викличте json.MarshalIndent(v, "", "\t") з пакету encoding/json — другий аргумент це префікс для кожного рядка (зазвичай порожній), третій — відступ на кожний рівень. Передайте "\t" для табів або " " для двох пробілів. Зовнішні бібліотеки не потрібні: encoding/json входить до стандартної бібліотеки Go.

Go
package main

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

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

У чому різниця між json.Marshal та json.MarshalIndent?

json.Marshal створює компактний однорядковий JSON без пробілів — ідеально для передачі по мережі, де важливий кожен байт. json.MarshalIndent приймає два додаткові рядкові параметри (prefix та indent) і створює читабельний форматований вивід. Обидві функції приймають однакові типи значень і повертають ([]byte, error). Єдина ціна MarshalIndent — трохи більше байт у виводі та зневажливо мала CPU-вартість для вставки пробілів.

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

Як форматувати JSON []byte без анмаршалінгу у структуру?

Використовуйте json.Indent(&buf, src, "", "\t"). Функція приймає готовий []byte з JSON і записує форматовану версію в bytes.Buffer — без визначення структури, без приведення типів, без проходу через Go-типи. Це найшвидший спосіб, коли вже є сирі JSON-байти, наприклад з тіла HTTP-відповіді або стовпця бази даних.

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

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

Чому json.MarshalIndent повертає помилку?

encoding/json повертає помилку, коли значення не може бути представлено у форматі JSON. Найпоширеніші причини: маршалінг каналу, функції або комплексного числа (вони не мають JSON-еквівалента); структура, що реалізує MarshalJSON() і повертає помилку; або map з не-рядковими ключами. Важливо: маршалінг структури з неекспортованим або nil-полем-вказівником помилки не викликає — вони просто пропускаються.

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

// Це поверне помилку — канали не серіалізуються в JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Це нормально — nil-поля з omitempty тихо пропускаються
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 пропущено, помилки немає

Як виключити поле з JSON-виводу у Go?

Є три способи. Перший: тег json:"-" — поле завжди виключається незалежно від значення. Другий: omitempty — поле виключається лише коли містить нульове значення для свого типу (nil-вказівник, порожній рядок, 0, false). Третій: неекспортовані (малі) поля автоматично виключаються encoding/json без жодних тегів.

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:"-"`                    // завжди виключено
	BillingName  string  `json:"billing_name,omitempty"` // виключено якщо порожнє
	internalRef  string                                 // неекспортоване — авто-виключення
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (порожній) та internalRef не з'являться у виводі

Як працювати з time.Time при JSON-маршалінгу?

encoding/json серіалізує time.Time у формат RFC3339Nano за замовчуванням (наприклад "2026-03-10T14:22:00Z"), сумісний з ISO 8601. Якщо потрібен інший формат — наприклад Unix-епоха як ціле число для legacy-API або власний формат дати — реалізуйте MarshalJSON() на типі-обгортці, що вбудовує time.Time, і повертайте потрібний формат.

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

// Поведінка за замовчуванням — RFC3339Nano, жодного коду не потрібно
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

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

// Кастомне: Unix-мітка часу як ціле число
type UnixTime struct{ time.Time }

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

Пов'язані інструменти

Також доступно на: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üllerТехнічний рецензент

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.