Почему JSON в Go выглядит как каша и как это исправить?
Вы когда-нибудь писали на Go программу, которая выводит JSON, и получали в терминале нечто подобное?
{"name":"PythonLib","year":2025,"tags":["go","json","tutorial"],"active":true}
Согласитесь, читать это без специальных инструментов — то ещё удовольствие. Особенно когда структура данных сложная, а вложенность уходит на несколько уровней. Глаза начинают разбегаться, и найти нужное поле становится настоящим квестом.
Хорошая новость: в Go есть встроенные средства, которые превращают эту «кашу» в аккуратный, структурированный документ. И сегодня мы разберём все способы красиво отформатировать JSON — от простого использования стандартной библиотеки до создания собственного pretty-print инструмента.
Этот материал основан на реальном вопросе с Stack Overflow (более 50 000 просмотров), так что тема действительно востребована. Поехали!
Что такое pretty-print и зачем он нужен?
Pretty-print (или «красивая печать») — это форматирование данных с отступами, переносами строк и выравниванием, чтобы их было удобно читать человеку. В контексте JSON это означает:
- Добавление пробелов или табуляции для вложенных элементов
- Перенос каждой пары «ключ-значение» на новую строку
- Выравнивание значений для наглядности
Зачем это нужно в Go? Представьте, что вы отлаживаете API-ответ, пишете логгер или просто выводите конфигурацию. Неотформатированный JSON занимает меньше места, но совершенно нечитаем. Pretty-print решает эту проблему за пару строк кода.
Способ 1: json.MarshalIndent — стандартное решение
Самый простой и рекомендуемый способ — использовать функцию json.MarshalIndent из стандартного пакета encoding/json. Она делает ровно то, что нужно: принимает данные и возвращает отформатированный JSON с отступами.
Вот базовый пример:
package main
import (
"encoding/json"
"fmt"
"log"
)
type Site struct {
Name string `json:"name"`
Year int `json:"year"`
Tags []string `json:"tags"`
Active bool `json:"active"`
}
func main() {
site := Site{
Name: "PythonLib",
Year: 2025,
Tags: []string{"go", "json", "tutorial"},
Active: true,
}
// Префикс — "", отступ — " " (два пробела)
prettyJSON, err := json.MarshalIndent(site, "", " ")
if err != nil {
log.Fatalf("Ошибка маршалинга: %v", err)
}
fmt.Println(string(prettyJSON))
}
Результат:
{
"name": "PythonLib",
"year": 2025,
"tags": [
"go",
"json",
"tutorial"
],
"active": true
}
Красиво, правда? Обратите внимание на второй и третий параметры MarshalIndent:
- Префикс — строка, которая добавляется в начало каждой строки. Обычно пустая строка
"". - Отступ — строка для каждого уровня вложенности. Чаще всего используют два пробела
" "или табуляцию"\t".
Настройка отступов: пробелы vs табуляция
Вы можете легко менять стиль отступов. Например, с табуляцией:
prettyJSON, _ := json.MarshalIndent(site, "", "\t")
fmt.Println(string(prettyJSON))
Результат:
{
"name": "PythonLib",
"year": 2025,
"tags": [
"go",
"json",
"tutorial"
],
"active": true
}
Какой вариант выбрать? Если ваш проект следует стандартам Go (а они рекомендуют табуляцию для кода), используйте "\t". Если вы работаете в команде, где приняты пробелы — два пробела будут уместнее. Главное — единообразие.
Способ 2: Pretty-print для сырых строк JSON
А что, если у вас уже есть строка с неотформатированным JSON? Например, вы получили её из HTTP-запроса или прочитали из файла. В этом случае сначала нужно распарсить строку в interface{}, а потом снова сериализовать с отступами.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
// Неотформатированный JSON
rawJSON := `{"name":"PythonLib","year":2025,"tags":["go","json","tutorial"],"active":true}`
// Шаг 1: распарсить в interface{}
var data interface{}
err := json.Unmarshal([]byte(rawJSON), &data)
if err != nil {
log.Fatalf("Ошибка парсинга: %v", err)
}
// Шаг 2: отформатировать
prettyJSON, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Fatalf("Ошибка форматирования: %v", err)
}
fmt.Println(string(prettyJSON))
}
Этот подход универсален: он работает с любым валидным JSON, независимо от структуры. Единственный минус — вы теряете информацию о типах (всё хранится как map[string]interface{}), но для pretty-print это неважно.
Способ 3: Кастомный pretty-print с помощью bytes.Buffer
Иногда нужно не просто вывести JSON в консоль, а записать его в файл, отправить по сети или использовать в логгере. Для таких случаев удобно использовать bytes.Buffer в паре с json.Indent.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
data := map[string]interface{}{
"name": "PythonLib",
"year": 2025,
"tags": []string{"go", "json", "tutorial"},
"nested": map[string]interface{}{
"active": true,
"score": 9.5,
},
}
// Сериализуем без форматирования
rawJSON, err := json.Marshal(data)
if err != nil {
log.Fatalf("Ошибка маршалинга: %v", err)
}
// Форматируем через буфер
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, rawJSON, "", " ")
if err != nil {
log.Fatalf("Ошибка индентации: %v", err)
}
fmt.Println(prettyBuf.String())
}
Преимущество этого метода — вы получаете bytes.Buffer, который можно использовать дальше: записать в файл, передать в логгер или даже отправить как HTTP-ответ.
Способ 4: Pretty-print для HTTP-ответов
Часто на практике нужно красиво вывести JSON-ответ от API. Вот пример с реальным запросом:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"