Как преобразовать Go struct в JSON: от новичка до профи
Работа с JSON — это хлеб насущный для любого бэкенд-разработчика на Go. Практически каждый день вам приходится отправлять данные клиенту, принимать запросы от API или сохранять конфигурации. И в 90% случаев это JSON. Но как грамотно превратить вашу родную struct в читаемый JSON? Казалось бы, что может быть проще? Однако дьявол кроется в деталях: неправильные теги, пустые поля, забытые ошибки — и вот уже ваш API возвращает не то, что нужно.
В этой статье мы разберем преобразование Go struct в JSON от самых азов до продвинутых техник. Вы узнаете, как работают теги, как скрывать пустые поля, как сериализовать вложенные структуры и как не упасть лицом в грязь с ошибками. Поехали!
Основы: пакет encoding/json
В стандартной библиотеке Go есть мощный пакет encoding/json. Его главные инструменты — функции json.Marshal (для превращения struct в JSON) и json.Unmarshal (для обратного процесса).
Давайте рассмотрим базовый пример. У нас есть структура пользователя:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Age int
Email string
}
func main() {
user := User{
Name: "Алексей",
Age: 30,
Email: "alex@example.com",
}
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println(string(jsonData))
// Вывод: {"Name":"Алексей","Age":30,"Email":"alex@example.com"}
}
Всё работает, но есть нюанс: ключи в JSON совпадают с именами полей структуры (с большой буквы). А это не соответствует стандартам JSON (обычно используют camelCase или snake_case).
Теги JSON: контроль над именами ключей
Чтобы управлять тем, как поля будут выглядеть в JSON, используются теги структуры. Это специальные аннотации, которые пишутся после типа поля в обратных кавычках.
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
// Теперь вывод будет:
// {"name":"Алексей","age":30,"email":"alex@example.com"}
Теги решают сразу несколько задач:
- Изменение имени поля — делаете ключи красивыми и читаемыми
- Пропуск поля — с помощью
json:"-"поле полностью исключается из JSON - Управление пустыми полями — опция
omitemptyскрывает поле, если оно содержит нулевое значение
Практический пример с omitempty
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
Email string `json:"email,omitempty"`
IsAdmin bool `json:"is_admin,omitempty"`
}
func main() {
user := User{Name: "Мария"}
jsonData, _ := json.Marshal(user)
fmt.Println(string(jsonData))
// Вывод: {"name":"Мария"}
// Age, Email и IsAdmin пропущены, так как они пустые
}
Обратите внимание: omitempty работает для нулевых значений — 0, false, пустая строка, nil. Это очень удобно для API, где не нужно тащить лишние поля.
Вложенные структуры и срезы
В реальных проектах данные редко бывают плоскими. Часто у вас есть вложенные объекты или массивы. Go справляется с этим на ура — пакет json рекурсивно обрабатывает все поля.
type Address struct {
City string `json:"city"`
Street string `json:"street"`
ZipCode string `json:"zip_code"`
}
type Company struct {
Name string `json:"name"`
Address Address `json:"address"`
Tags []string `json:"tags"`
}
func main() {
company := Company{
Name: "TechCorp",
Address: Address{
City: "Москва",
Street: "Тверская, 1",
ZipCode: "101000",
},
Tags: []string{"IT", "Fintech", "AI"},
}
jsonData, _ := json.MarshalIndent(company, "", " ")
fmt.Println(string(jsonData))
// Вывод:
// {
// "name": "TechCorp",
// "address": {
// "city": "Москва",
// "street": "Тверская, 1",
// "zip_code": "101000"
// },
// "tags": ["IT", "Fintech", "AI"]
// }
}
Функция json.MarshalIndent — ваш лучший друг для отладки. Она добавляет отступы и делает JSON читаемым. Для продакшена используйте обычный Marshal — он компактнее.
Сериализация карт (map) и интерфейсов
Иногда структура данных динамическая, и вы не можете заранее описать её типом. В таких случаях на помощь приходят map[string]interface{} или any (с Go 1.18).
func main() {
data := map[string]interface{}{
"name": "Динамический объект",
"count": 42,
"active": true,
"metadata": map[string]interface{}{
"version": "1.0",
"author": "Go Developer",
},
}
jsonData, _ := json.Marshal(data)
fmt.Println(string(jsonData))
// {"active":true,"count":42,"metadata":{"author":"Go Developer","version":"1.0"},"name":"Динамический объект"}
}
Это мощный инструмент, но используйте его с осторожностью: вы теряете типовую безопасность, и при десериализации придётся делать приведение типов вручную.
Обработка ошибок: почему это важно?
Многие новички пишут json.Marshal(user) и игнорируют возвращаемую ошибку. Это путь к боли. Ошибка может возникнуть, если:
- Поле содержит значение, которое нельзя сериализовать (например, канал или функция)
- Есть циклические ссылки
- Числа с плавающей точкой превращаются в
NaNилиInf
type BadStruct struct {
Ch chan int `json:"ch"`
}
func main() {
bad := BadStruct{Ch: make(chan int)}
_, err := json.Marshal(bad)
if err != nil {
fmt.Println("Ошибка:", err)
// Вывод: json: unsupported type: chan int
}
}
Всегда обрабатывайте ошибку — это спасёт вам часы отладки.
Пользовательская сериализация: интерфейс Marshaler
Иногда стандартное поведение не подходит. Например, нужно вывести дату в особом формате или замаскировать пароль. Для этого реализуйте интерфейс json.Marshaler:
type SafeUser struct {
Name string
Password string