Avatar ·

Как правильно преобразовать структуру Go в JSON и обработать ошибки?

Всем привет! Я только начал изучать Go и столкнулся с проблемой при работе с JSON. Пытаюсь преобразовать структуру в JSON, но ничего не работает. Вот мой код: ```go package main import ( "fmt" "encoding/json" ) type Person struct { Name string Age int City string } func main() { p := Person{Name: "Иван", Age: 30, City: "Москва"} // Пытаюсь преобразовать в JSON result := json.Marshal(p) fmt.Println(result) } ``` Вместо нормального JSON я получаю странный вывод вроде `[123 34 78 97...]`. Я ожидал увидеть что-то типа `{"Name":"Иван","Age":30,"City":"Москва"}`. Почему так происходит? Я читал документацию, но там всё сложно написано. Может быть, нужно какие-то теги добавлять? Или я неправильно вызываю функцию? Объясните, пожалуйста, как правильно сериализовать структуру в JSON в Go. Ещё вопрос: что делать, если в структуре есть вложенные структуры или срезы? Как быть с приватными полями? И как обрабатывать ошибки при маршалинге? Буду благодарен за подробное объяснение с примерами. <<>>

Основная проблема: json.Marshal возвращает []byte, а не строку

Вы почти всё сделали правильно, но забыли одну важную деталь: функция json.Marshal() возвращает срез байтов ([]byte), а не строку. Именно поэтому вы видите числа — это коды символов JSON в виде байтов. Чтобы получить читаемый JSON, нужно преобразовать результат в строку:

package main

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

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    City string `json:"city"`
}

func main() {
    p := Person{
        Name: "Иван",
        Age:  30,
        City: "Москва",
    }

    // Правильный способ: Marshal возвращает ([]byte, error)
    jsonBytes, err := json.Marshal(p)
    if err != nil {
        log.Fatalf("Ошибка маршалинга: %v", err)
    }

    // Преобразуем байты в строку
    fmt.Println(string(jsonBytes))
    // Вывод: {"name":"Иван","age":30,"city":"Москва"}
}

Теги JSON: контроль над именами полей

Обратите внимание на теги json:"name" после полей структуры. Они позволяют:

  • Задать кастомные имена полей в JSON (обычно в camelCase)
  • Скрыть поле с помощью json:"-"
  • Указать, что поле необязательно — json:"name,omitempty"
type User struct {
    ID        int    `json:"id"`
    Username  string `json:"username"`
    Password  string `json:"-"`              // Никогда не будет в JSON
    Email     string `json:"email,omitempty"` // Пропустится, если пустое
    CreatedAt string `json:"created_at"`
}

Работа с вложенными структурами и срезами

Go отлично сериализует вложенные структуры, срезы и карты — это происходит рекурсивно:

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
}

type Company struct {
    Name    string    `json:"name"`
    Address Address   `json:"address"`
    Tags    []string  `json:"tags"`
    Numbers []int     `json:"numbers"`
}

func main() {
    company := Company{
        Name: "Acme Corp",
        Address: Address{
            Street: "Ленина, 10",
            City:   "Москва",
        },
        Tags:    []string{"IT", "Go", "Backend"},
        Numbers: []int{1, 2, 3},
    }

    jsonData, _ := json.MarshalIndent(company, "", "  ")
    fmt.Println(string(jsonData))
    // Вывод:
    // {
    //   "name": "Acme Corp",
    //   "address": {
    //     "street": "Ленина, 10",
    //     "city": "Москва"
    //   },
    //   "tags": ["IT", "Go", "Backend"],
    //   "numbers": [1, 2, 3]
    // }
}

Обратите внимание на json.MarshalIndent — она добавляет форматирование с отступами для читаемости.

Приватные поля: почему они не сериализуются

Поля структуры, начинающиеся с маленькой буквы (приватные), не будут включены в JSON. Это особенность пакета encoding/json — он работает только с экспортируемыми (публичными) полями:

type SecretData struct {
    PublicField  string `json:"public"`   // Будет в JSON
    privateField string                   // Не будет! (начинается с маленькой)
}

Обработка ошибок: всегда проверяйте err!

Функция json.Marshal может вернуть ошибку в следующих случаях:

  • Поле содержит значение, которое не может быть представлено в JSON (например, канал, функция, комплексное число)
  • Рекурсивная структура (циклические ссылки)
  • Неподдерживаемые типы данных
// Пример с ошибкой: канал не сериализуется
type BadStruct struct {
    Ch chan int
}

data := BadStruct{Ch: make(chan int)}
jsonBytes, err := json.Marshal(data)
if err != nil {
    log.Printf("Ошибка: %v", err)
    // Вывод: json: unsupported type: chan int
}

Полезные советы для начинающих

  • Всегда проверяйте ошибки — это спасёт от неожиданного падения программы
  • Используйте json.MarshalIndent для отладки
  • Добавляйте теги json для контроля над именами полей
  • Для больших структур используйте json.NewEncoder(w).Encode(v) — это эффективнее по памяти
  • Если нужно работать с произвольным JSON — используйте map[string]interface{} или json.RawMessage
<<>>

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

Если вы работаете с большими объёмами данных или пишете JSON сразу в файл/сеть, используйте json.NewEncoder. Он не создаёт промежуточный срез байтов, а пишет напрямую в io.Writer:

package main

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

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {
    products := []Product{
        {ID: 1, Name: "Ноутбук", Price: 999.99},
        {ID: 2, Name: "Мышь", Price: 25.50},
    }

    // Запись в stdout
    encoder := json.NewEncoder(os.Stdout)
    encoder.SetIndent("", "  ") // Для форматирования

    if err := encoder.Encode(products); err != nil {
        fmt.Printf("Ошибка: %v\n", err)
    }
}

Когда использовать Marshal, а когда Encoder?

  • json.Marshal — когда нужно получить JSON как строку/срез байтов для дальнейшей обработки (например, передать в HTTP-ответ)
  • json.NewEncoder — когда пишете JSON напрямую в файл, сокет, HTTP-ответ (экономит память, не создавая гигантские срезы)

Также не забывайте про json.RawMessage для отложенной сериализации — это полезно, когда часть JSON вы хотите сформировать позже или уже имеете сырые JSON-данные.