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: "a.ivanov@primer.ru",
DisplayName: "Алексей Иванов", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "a.ivanov@primer.ru",
// "display_name": "Алексей Иванов",
// "is_admin": true
// }
// Пользователь без опциональных полей — они полностью пропускаются
minimal := UserProfile{ID: "usr_2a91", Email: "m.sokolova@primer.ru"}
// {
// "id": "usr_2a91",
// "email": "m.sokolova@primer.ru"
// }Тег 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: "RUB"},
Tax: Money{Amount: 1592, Currency: "RUB"},
Total: Money{Amount: 21492, Currency: "RUB"},
}
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": "RUB", "display": "RUB 199.00" },
// "tax": { "amount": 15.92, "currency": "RUB", "display": "RUB 15.92" },
// "total": { "amount": 214.92, "currency": "RUB", "display": "RUB 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-dannyh.json"); err != nil {
log.Fatalf("format config: %v", err)
}
fmt.Println("config/baza-dannyh.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: "schot.skachat", 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": "ru-central-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "ru-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/scheta","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/scheta",
// "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-эпоха в виде целого числа для легаси-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: "schot.oplachen",
OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "schot.oplachen",
// "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.