Магия JSON в Go: почему одни поля видны, а другие — нет?
Вы когда-нибудь сталкивались с ситуацией, когда ваша Go-структура отправляет в JSON-ответе то, что клиенту видеть совсем не обязательно? Например, пароль пользователя, внутренний ID базы данных или секретный ключ. Или, наоборот, нужно показать поле только при определённом условии (например, если оно не пустое).
Эта проблема — одна из самых популярных на Stack Overflow, и не зря. Работа с JSON в Go — это искусство баланса между строгостью типов и гибкостью представления. Сегодня мы разберём все способы: от простого тега json:"-" до сложных кастомных решений с интерфейсами. Поехали!
1. Самый простой способ: тег json:"-"
Go позволяет управлять сериализацией структуры через специальные теги. Если вы хотите навсегда скрыть поле из JSON, просто добавьте json:"-".
type User struct {
Name string `json:"name"`
Password string `json:"-"`
Age int `json:"age"`
}
func main() {
u := User{Name: "Алиса", Password: "supersecret", Age: 30}
data, _ := json.Marshal(u)
fmt.Println(string(data))
// Вывод: {"name":"Алиса","age":30}
}
Поле Password исчезло, как будто его и не было. Идеально для конфиденциальных данных, которые никогда не должны покидать сервер.
2. Условное скрытие: omitempty
Иногда нужно скрыть поле только если оно пустое. Для этого используется опция omitempty (сокращение от "omit if empty").
type Product struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Price float64 `json:"price,omitempty"`
}
func main() {
p1 := Product{Name: "Книга", Price: 500.0}
data1, _ := json.Marshal(p1)
fmt.Println(string(data1)) // {"name":"Книга","price":500}
p2 := Product{Name: "Курс"}
data2, _ := json.Marshal(p2)
fmt.Println(string(data2)) // {"name":"Курс"}
}
Важно: omitempty работает только с "нулевыми" значениями: "", 0, false, nil (для слайсов, мапов, указателей).
Ловушка с числовыми полями
Будьте осторожны: если Price равен 0, он тоже скроется. В некоторых бизнес-логиках это недопустимо (бесплатный товар — это тоже цена). В таких случаях лучше использовать указатели.
3. Использование указателей (*) и интерфейсов
Указатели — ваш лучший друг, когда нужно различать "значение не указано" и "значение равно нулю".
type UserProfile struct {
Name string `json:"name"`
Age *int `json:"age,omitempty"` // указатель на int
Bio *string `json:"bio,omitempty"`
}
func main() {
// Возраст не указан — поле скроется
u1 := UserProfile{Name: "Иван"}
data1, _ := json.Marshal(u1)
fmt.Println(string(data1)) // {"name":"Иван"}
// Возраст = 0 — это осмысленное значение
zero := 0
u2 := UserProfile{Name: "Мария", Age: &zero}
data2, _ := json.Marshal(u2)
fmt.Println(string(data2)) // {"name":"Мария","age":0}
}
Этот подход часто используется в REST API, где нужно обновлять только переданные поля (частичное обновление, PATCH-запросы).
4. Кастомная сериализация: интерфейс json.Marshaler
Если стандартных тегов недостаточно, можно полностью контролировать процесс. Реализуйте интерфейс json.Marshaler и напишите свою логику.
type DynamicUser struct {
ID int
Name string
ShowEmail bool
Email string
}
func (u DynamicUser) MarshalJSON() ([]byte, error) {
// Создаём анонимную структуру только для сериализации
m := map[string]interface{}{
"id": u.ID,
"name": u.Name,
}
if u.ShowEmail {
m["email"] = u.Email
}
return json.Marshal(m)
}
func main() {
u1 := DynamicUser{ID: 1, Name: "Алиса", ShowEmail: true, Email: "alice@example.com"}
data1, _ := json.Marshal(u1)
fmt.Println(string(data1)) // {"email":"alice@example.com","id":1,"name":"Алиса"}
u2 := DynamicUser{ID: 2, Name: "Боб", ShowEmail: false, Email: "bob@example.com"}
data2, _ := json.Marshal(u2)
fmt.Println(string(data2)) // {"id":2,"name":"Боб"}
}
Минус: такой код сложнее поддерживать, но он даёт полную свободу. Например, можно переименовывать поля, объединять их или менять типы на лету.
5. Продвинутая техника: встраивание структур (embedding) и переопределение
Иногда нужно взять большую структуру и убрать из неё несколько полей. Вместо того чтобы копировать все поля, используйте встраивание и переопределите "лишние" поля.
type FullUser struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"password"`
Email string `json:"email"`
}
// PublicUser — безопасная версия для API
type PublicUser struct {
FullUser // встраиваем все поля
Password string `json:"-"` // переопределяем поле, чтобы скрыть его
}
func main() {
fu := FullUser{ID: 1, Name: "Алиса", Password: "secret", Email: "a@b.com"}
pu := PublicUser{FullUser: fu}
data, _ := json.Marshal(pu)
fmt.Println(string(data)) // {"id":1,"name":"Алиса","email":"a@b.com"}
}
Этот метод элегантен, когда у вас есть две версии одной сущности: полная (для внутреннего использования) и публичная (для API).
6. Динамическое удаление полей: слайсы и мапы вместо структур
Если структура данных постоянно меняется, можно вообще отказаться от структур и использовать map[string]interface{}. Но это крайняя мера — вы теряете типизацию и автодополнение в IDE.
func buildResponse(includeEmail bool) map[string]interface{} {
resp := map[string]interface{}{
"name": "Иван",
"age": 25,
}
if includeEmail {
resp["email"] = "ivan@example.com"
}
return resp
}
func main() {
data1, _ := json.Marshal(buildResponse(true))
fmt.Println(string(data1)) // {"age":25,"email":"ivan@example.com","name":"Иван"}
}
Подходит для простых скриптов или прототипов, но в production-коде лучше использовать структуры.
Сравнительная таблица методов
Чтобы вы могли быстро выбрать подходящий способ, вот краткая шпаргалка:
-
Блоги
Рекомендаций по книгам
Читайте также
Code review в Java: что проверять и как давать обратную связь
Кэш в Go: враг тестирования? Разбираем force retesting
Тестирование в Java: unit, integration, e2e — полное руководство
Move View with Keyboard Swift: Оптимизация и производительность
Мониторинг C++ приложений: метрики и алерты продвинутого уровня