> Какой интерфейс реализовать для JSON маршаллинга и анмаршаллинга в Go и почему методы должны иметь ресивер по указателю (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Лига Цифровой Экономики

Стек: Go

> Пример ответа

Короткий ответ

Для JSON маршаллинга и анмаршаллинга в Go нужно реализовать интерфейсы json.Marshaler и json.Unmarshaler с методами MarshalJSON() ([]byte, error) и UnmarshalJSON([]byte) error соответственно. Ресивер по указателю обязателен для UnmarshalJSON, потому что метод должен мутировать получателя. Для MarshalJSON указатель тоже предпочтителен: это позволяет избежать копирования больших структур и гарантирует консистентность, если метод зависит от состояния, которое может меняться.

Подробное объяснение

Интерфейсы json.Marshaler и json.Unmarshaler - это стандартный механизм кастомизации JSON-сериализации в Go. Когда тип реализует MarshalJSON, пакет encoding/json вызывает этот метод вместо стандартного рефлексивного обхода структуры. Аналогично UnmarshalJSON перехватывает процесс десериализации.

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

  1. Мутация для UnmarshalJSON - метод обязан изменять поля получателя. При ресивере по значению Go передаёт копию, и все изменения будут потеряны после возврата из метода. Это не просто рекомендация, а жёсткое требование компилятора: метод с ресивером по значению не удовлетворяет интерфейсу json.Unmarshaler, потому что json.Unmarshal работает с указателем на значение.

  2. Избежание копирования для MarshalJSON - при маршаллинге больших структур копирование каждого элемента может быть дорогим. Ресивер по указателю позволяет работать с оригиналом.

  3. Консистентность состояния - если MarshalJSON зависит от полей, которые могут быть изменены конкурентно, указатель даёт доступ к актуальному состоянию, а не к снимку на момент вызова.

  4. Семантика nil - при ресивере по указателю метод может корректно обрабатывать nil-получателя, возвращая null или ошибку. С ресивером по значению nil-случай вообще не может быть обработан.

Важно: если тип реализует MarshalJSON только на указателе, то маршаллинг значения (не указателя) будет использовать стандартное рефлексивное поведение, а не ваш метод. Это часто приводит к неожиданным багам.

На практике

В реальном коде обычно реализуют оба метода на указателе. Это даёт единообразное поведение и для значений, и для указателей. Если нужно, чтобы и значение, и указатель использовали кастомную сериализацию, можно определить метод на значении для MarshalJSON, но для UnmarshalJSON это невозможно - только указатель.

Частая практика - использовать кастомные типы-обёртки для полей с особыми требованиями (например, время в определённом формате, кастомные enum, числа с точностью). Также часто реализуют MarshalJSON для типов-алиасов, чтобы изменить формат без изменения исходной структуры.

Ещё один нюанс: если внутри MarshalJSON вы вызываете json.Marshal на самом типе, который реализует этот интерфейс, вы получите бесконечную рекурсию. Нужно использовать промежуточный тип-алиас без методов.

Пример кода

GO
type CustomTime struct {
time.Time
}
// MarshalJSON - ресивер по указателю
func (ct *CustomTime) MarshalJSON() ([]byte, error) {
if ct == nil {
return []byte("null"), nil
}
return json.Marshal(ct.Format("2006-01-02"))
}
// UnmarshalJSON - ресивер по указателю, обязателен
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsed, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
ct.Time = parsed
return nil
}
type Event struct {
ID int `json:"id"`
Date CustomTime `json:"date"`
}
func main() {
e := Event{ID: 1, Date: CustomTime{Time: time.Now()}}
b, _ := json.Marshal(&e)
fmt.Println(string(b)) // {"id":1,"date":"2025-01-15"}
var e2 Event
json.Unmarshal([]byte(`{"id":2,"date":"2025-01-16"}`), &e2)
fmt.Println(e2.Date.Format("2006-01-02")) // 2025-01-16
}

Как отвечать на собеседовании

Начните с прямого ответа: назовите интерфейсы и методы. Затем объясните, почему указатель - это не просто выбор, а необходимость для UnmarshalJSON. Подчеркните разницу между маршаллингом и анмаршаллингом: первый может работать и на значении, второй - только на указателе. Приведите пример из практики, где кастомная сериализация решает реальную проблему (формат даты, обработка null, совместимость с API). Упомяните про риск рекурсии при вызове json.Marshal внутри MarshalJSON и как его избежать через алиас типа. Если спросят про производительность, скажите, что указатель избегает копирования, но для маленьких структур это не критично.

Что проверяет интервьюер

Интервьюер проверяет:

  • Понимание механизма интерфейсов в Go и того, как encoding/json с ними взаимодействует.
  • Знание семантики ресиверов: мутация, копирование, nil-обработка.
  • Умение предвидеть подводные камни: рекурсия, различие поведения для значения и указателя.
  • Практический опыт: способность привести реальный кейс использования кастомной сериализации.
  • Понимание trade-off между кастомной реализацией и стандартным рефлексивным поведением.

Типичные ошибки

  • Реализация UnmarshalJSON с ресивером по значению - код не компилируется или молча не работает.
  • Забывают про nil-получатель в MarshalJSON, что приводит к panic при маршаллинге nil-указателя.
  • Вызов json.Marshal внутри MarshalJSON без промежуточного типа - бесконечная рекурсия и stack overflow.
  • Реализация только MarshalJSON без UnmarshalJSON - асимметричное поведение при сериализации и десериализации.
  • Ожидание, что метод на указателе будет вызван для значения - это не так, нужно явно брать адрес.
  • Игнорирование ошибок внутри UnmarshalJSON - возврат nil при некорректных данных, что маскирует проблемы.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью