Avatar ·

Как красиво отформатировать JSON в Go: подробное руководство

Привет! Я новичок в Go и работаю над проектом, где нужно выводить JSON в читаемом виде. У меня есть структура данных, которую я сериализую в JSON с помощью `json.Marshal()`, но на выходе получается однострочная строка без отступов и переносов. Это очень неудобно для отладки и логов. Вот мой код: ```go package main import ( "encoding/json" "fmt" ) type User struct { Name string Age int Email string Address Address } type Address struct { City string Country string } func main() { user := User{ Name: "Иван Иванов", Age: 30, Email: "ivan@example.com", Address: Address{ City: "Москва", Country: "Россия", }, } jsonData, err := json.Marshal(user) if err != nil { fmt.Println("Ошибка:", err) return } fmt.Println(string(jsonData)) } ``` Результат: `{"Name":"Иван Иванов","Age":30,"Email":"ivan@example.com","Address":{"City":"Москва","Country":"Россия"}}` Я пробовал использовать `json.MarshalIndent`, но не совсем понял, как правильно задавать префикс и отступы. Подскажите, пожалуйста, как сделать вывод красивым — с переносами строк и отступами, как в обычном pretty-print JSON? Желательно с примерами для разных ситуаций: вывод в консоль, запись в файл, и для логов. Заранее спасибо! <<>>

Основное решение: json.MarshalIndent

В Go для красивого форматирования JSON существует встроенная функция json.MarshalIndent из пакета encoding/json. Она принимает три аргумента: данные для сериализации, префикс (обычно пустая строка) и отступ (чаще всего табуляция или два пробела).

Исправленный пример из вашего кода:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Email   string  `json:"email"`
    Address Address `json:"address"`
}

type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

func main() {
    user := User{
        Name:  "Иван Иванов",
        Age:   30,
        Email: "ivan@example.com",
        Address: Address{
            City:    "Москва",
            Country: "Россия",
        },
    }

    // Используем json.MarshalIndent
    jsonData, err := json.MarshalIndent(user, "", "  ") // отступ — 2 пробела
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    fmt.Println(string(jsonData))
}

Результат:

{
  "name": "Иван Иванов",
  "age": 30,
  "email": "ivan@example.com",
  "address": {
    "city": "Москва",
    "country": "Россия"
  }
}

Параметры MarshalIndent

  • Первый параметр (data) — любые данные, которые можно сериализовать в JSON (структура, map, slice и т.д.).
  • Второй параметр (prefix) — строка, которая добавляется в начало каждой строки. Обычно пустая строка "". Можно использовать, например, "> " для создания отступа в виде символов.
  • Третий параметр (indent) — строка отступа для каждого уровня вложенности. Чаще всего " " (два пробела) или "\t" (табуляция).

Пример с табуляцией

jsonData, _ := json.MarshalIndent(user, "", "\t")
fmt.Println(string(jsonData))

Результат:

{
	"name": "Иван Иванов",
	"age": 30,
	"email": "ivan@example.com",
	"address": {
		"city": "Москва",
		"country": "Россия"
	}
}

Запись в файл с форматированием

package main

import (
    "encoding/json"
    "os"
)

type Config struct {
    Port int    `json:"port"`
    Host string `json:"host"`
}

func main() {
    cfg := Config{Port: 8080, Host: "localhost"}

    file, err := os.Create("config.json")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ") // устанавливаем отступы для encoder
    if err := encoder.Encode(cfg); err != nil {
        panic(err)
    }
}

Pretty-print для логов

package main

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

func main() {
    data := map[string]interface{}{
        "message": "Hello",
        "level":   "info",
        "data":    []int{1, 2, 3},
    }

    var buf bytes.Buffer
    encoder := json.NewEncoder(&buf)
    encoder.SetIndent("", "  ")
    encoder.Encode(data)

    log.Println(buf.String())
}

Важные замечания

  • Для красивого вывода используйте теги json:"fieldname" в структурах, чтобы управлять именами полей в JSON (как показано в примере выше).
  • Функция MarshalIndent работает медленнее, чем Marshal, поэтому для production-кода, где производительность критична, используйте обычный Marshal, а pretty-print применяйте только для отладки.
  • Для больших объёмов данных используйте json.Encoder с SetIndent, чтобы не держать весь JSON в памяти.
<<>>

Альтернативный подход: использование json.Encoder с кастомизацией

Если вы работаете с потоками данных или хотите больше контроля над форматированием, используйте json.NewEncoder. Он позволяет задавать отступы через метод SetIndent и писать JSON напрямую в io.Writer (консоль, файл, буфер).

package main

import (
    "encoding/json"
    "os"
)

func main() {
    user := map[string]interface{}{
        "name": "Иван",
        "age":  30,
        "address": map[string]string{
            "city": "Москва",
        },
    }

    encoder := json.NewEncoder(os.Stdout)
    encoder.SetIndent("", "    ") // 4 пробела
    encoder.Encode(user)
}

Результат:

{
    "address": {
        "city": "Москва"
    },
    "age": 30,
    "name": "Иван"
}

Когда использовать этот подход

  • Когда нужно выводить JSON в лог или в ответ HTTP-запроса без предварительного создания строки.
  • Когда данные большие и не помещаются в память — Encoder работает потоково.
  • Если нужно добавить префикс (например, для логов с меткой времени).

Недостаток: Encoder.Encode автоматически добавляет символ новой строки \n в конце, что может быть нежелательно при записи в файл с другим форматированием. В таких случаях используйте MarshalIndent.