JSON Formatter Go — Посібник MarshalIndent()
Використовуйте безкоштовний 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.
{"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" для табів або " " для двох пробілів.
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-нативному інструментарії
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:"-" виключає поле повністю — корисно для паролів, внутрішніх ідентифікаторів або полів, що не повинні покидати межу сервісу.
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-поля або дані, правильні в пам'яті, але не призначені для серіалізації у зовнішні системи.
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 з конвертацією копійок у гривні
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 — рядкове представлення
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-виводі.
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"
// }[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 тут немає.
Поширені опції struct tags:
json.Indent() — переформатування готових JSON-байт
Якщо вже є []byte з JSON — наприклад з тіла HTTP-відповіді, стовпця Postgres jsonb або файлу, зчитаного через os.ReadFile — не потрібно визначати структуру і виконувати анмаршалінг перед форматуванням. json.Indent безпосередньо переформатовує сирі байти, записуючи форматований вивід у bytes.Buffer.
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-ів з форматуванням при читанні, коли визначення структури недоступне.
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, записати назад або логувати.
Читаємо файл → форматуємо → записуємо назад
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-логування
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 (структура не потрібна)
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 і доступу до полів — це виключно візуальна трансформація. Обидва підходи коректно обробляють великі тіла відповідей, поки все тіло вміщується в пам'ять.
// Патерн 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-програму. Ось однорядники, що відклалися в м'язовій пам'яті під час розробки та розбору інцидентів.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Лише форматування cat api-response.json | jq . # Витягти вкладене поле cat api-response.json | jq '.checks.database' # Фільтрувати масив cat audit-log.json | jq '.[] | select(.severity == "error")'
# 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.gogofmt форматує 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.
go get github.com/goccy/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
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-об'єкт на рядок
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)
}
}os.ReadFile виділить увесь буфер у купі, створить тиск на GC і може викликати OOM на контейнерах з обмеженою пам'яттю.Поширені помилки
Проблема: Відкидання помилки через _ означає, що несеріалізоване значення (канал, функція, комплексне число) мовчки створить nil-вивід або викличе паніку нижче по коду при виклику string(nil).
Рішення: Завжди перевіряйте помилку. Якщо серіалізуєте тип, що має завжди успішно серіалізуватися, паніка через log.Fatalf краща за мовчазну втрату даних.
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(string(data)) додає символ переносу рядка після JSON, що пошкоджує pipeline-и, які обробляють вивід як сирі байти — наприклад, при передачі в jq або записі в бінарний протокол.
Рішення: Використовуйте os.Stdout.Write(data) для бінарно-чистого виводу. Якщо потрібен завершальний перенос рядка для відображення людині, додайте його явно.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // додає зайвий перенос рядка в кінці
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // явний перенос рядка лише коли потрібенПроблема: Без omitempty nil-*string або *int-вказівник серіалізується як "field": null. Це розкриває внутрішні імена полів і може зламати суворі JSON-schema-валідатори на стороні споживача.
Рішення: Додайте omitempty до полів-вказівників, які мають бути відсутні (не null) у виводі. nil *T з omitempty взагалі не створює ключа в JSON.
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]any втрачає інформацію про типи, вимагає ручних type-assertions і створює недетермінований порядок ключів — що ускладнює JSON-дифференціювання та порівняння логів.
Рішення: Визначте структуру з правильними json-тегами. Структури типобезпечні, маршалються швидше, створюють детермінований порядок полів відповідно до визначення структури і роблять код самодокументованим.
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.MarshalIndent коли контролюєте визначення структури і потрібен форматований вивід — конфігураційні файли, debug-логування, test-fixture-и та логування API-відповідей. Використовуйте json.Indent коли вже є сирі байти і потрібно просто додати пробіли без проходу через Go-типи. Переходьте на go-json або sonic лише після того, як профілювання підтвердило, що JSON-маршалінг є вимірним вузьким місцем — для більшості сервісів стандартної бібліотеки більш ніж достатньо.
Часті запитання
Як форматувати JSON у Go?
Викличте json.MarshalIndent(v, "", "\t") з пакету encoding/json — другий аргумент це префікс для кожного рядка (зазвичай порожній), третій — відступ на кожний рівень. Передайте "\t" для табів або " " для двох пробілів. Зовнішні бібліотеки не потрібні: encoding/json входить до стандартної бібліотеки 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-вартість для вставки пробілів.
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-відповіді або стовпця бази даних.
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-полем-вказівником помилки не викликає — вони просто пропускаються.
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 без жодних тегів.
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, і повертайте потрібний формат.
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
}Пов'язані інструменти
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.