Avatar ·

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

Всем привет! Я начинающий Go-разработчик и столкнулся с проблемой при конвертации структуры в JSON. Пишу простой сервис для управления пользователями, и мне нужно сериализовать данные в JSON для отправки через HTTP. Вот моя структура: ```go type User struct { Name string Age int Email string } ``` Я использую `json.Marshal()`: ```go user := User{Name: "Иван", Age: 30, Email: "ivan@example.com"} data, err := json.Marshal(user) if err != nil { log.Fatal(err) } fmt.Println(string(data)) ``` Но результат меня смущает — поля выводятся с заглавной буквы: `{"Name":"Иван","Age":30,"Email":"ivan@example.com"}`. В документации я видел, что можно использовать теги, но не понимаю, как их правильно применять. Кроме того, как быть с полями, которые могут быть пустыми? Например, если Email не указан, я хочу исключить его из JSON. И ещё: как обрабатывать вложенные структуры и массивы? Подскажите, пожалуйста, лучшие практики для работы с JSON в Go. Может быть, есть какие-то хитрости с кастомными типами или обработкой ошибок? Буду благодарен за примеры кода! <<>>

Основы сериализации структур в JSON

Ваша проблема с заглавными буквами в JSON решается с помощью тегов структур. В Go теги позволяют задать имена полей при сериализации. Вот как это сделать правильно:

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

Ключевые моменты:

  • json:"name" — задаёт имя поля в JSON
  • omitempty — исключает поле, если оно имеет нулевое значение (пустая строка, 0, nil, false)
  • Если нужно игнорировать поле: json:"-"

Полный пример с вложенными структурами

Рассмотрим более сложный случай:

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

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Age       int       `json:"age,omitempty"`
    Email     string    `json:"email,omitempty"`
    Address   Address   `json:"address"`
    Tags      []string  `json:"tags,omitempty"`
    CreatedAt time.Time `json:"created_at"`
}

func main() {
    user := User{
        ID:    1,
        Name:  "Иван",
        Age:   30,
        Email: "",
        Address: Address{
            City:    "Москва",
            Country: "",
        },
        Tags: nil,
        CreatedAt: time.Now(),
    }

    data, err := json.MarshalIndent(user, "", "  ")
    if err != nil {
        log.Fatalf("Ошибка сериализации: %v", err)
    }
    fmt.Println(string(data))
}

Вывод:

{
  "id": 1,
  "name": "Иван",
  "address": {
    "city": "Москва"
  },
  "created_at": "2024-01-15T10:30:00Z"
}

Обработка ошибок и кастомные типы

Ошибки json.Marshal() обычно возникают при:

  • Циклических ссылках в структурах
  • Неподдерживаемых типах (например, каналы, функции)
  • Больших числах (float64 с большим количеством знаков)

Для кастомной сериализации реализуйте интерфейс json.Marshaler:

type SafeString string

func (s SafeString) MarshalJSON() ([]byte, error) {
    // Экранируем опасные символы
    sanitized := strings.ReplaceAll(string(s), "&", "&")
    return json.Marshal(sanitized)
}

type Post struct {
    Title   string     `json:"title"`
    Content SafeString `json:"content"`
}

Работа с пустыми полями и zero values

Иногда нужно явно указать, что поле должно быть в JSON, даже если оно пустое. Используйте указатели:

type Config struct {
    Timeout *int `json:"timeout"`
}

timeout := 0
cfg := Config{Timeout: &timeout} // будет {"timeout":0}
// cfg := Config{} // будет {"timeout":null} или {} если omitempty

Лучшие практики

  • Всегда используйте теги json с нижним регистром (snake_case)
  • Добавляйте omitempty для необязательных полей
  • Используйте json.MarshalIndent для отладки
  • Для больших данных используйте json.Encoder с буферизацией
  • Проверяйте ошибки — игнорирование может привести к панике
// Пример с Encoder для потоковой записи
func writeJSON(w io.Writer, v interface{}) error {
    encoder := json.NewEncoder(w)
    encoder.SetIndent("", "  ")
    return encoder.Encode(v)
}
<<>>

Альтернативный подход: использование map для динамических данных

Иногда структура не определена заранее или нужно генерировать JSON из динамических данных. В таких случаях удобно использовать map[string]interface{}:

func buildUserJSON(name string, age int, email string) ([]byte, error) {
    data := map[string]interface{}{
        "name": name,
        "age":  age,
    }
    
    if email != "" {
        data["email"] = email
    }
    
    return json.Marshal(data)
}

Работа с временными метками и форматами

По умолчанию time.Time сериализуется в RFC3339. Если нужен другой формат, создайте кастомный тип:

type CustomTime struct {
    time.Time
}

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    return json.Marshal(ct.Format("2006-01-02"))
}

type Event struct {
    Name string     `json:"name"`
    Date CustomTime `json:"date"`
}

Использование библиотек для сложных случаев

Для больших проектов рассмотрите библиотеки:

  • json-iterator/go — быстрее стандартного encoding/json
  • goccy/go-json — ещё более производительная
  • tidwall/gjson — для чтения JSON без полной десериализации

Но для 90% задач стандартного пакета encoding/json более чем достаточно. Главное — правильно использовать теги и обрабатывать ошибки.