JSON: encoding/json, теги и custom marshal
Зачем это на интервью
JSON — граница API и хранения. Вопрос проверяет, умеет ли кандидат отделить внешний контракт от Go-модели, валидировать вход и не скрывать ошибки сериализации.
Минимум для E4
- Использовать экспортируемые поля и struct tags.
- Понимать
omitemptyи различиеnil/пустого slice в JSON. - Проверять ошибку каждого
Encode/Decode.
Углубление для E5/Senior
encoding/json по умолчанию терпимо к лишним полям и при повторяющихся ключах действует по правилам пакета; на границе строгого API решите это явно (Decoder.DisallowUnknownFields). Decode одного значения из network body не доказывает отсутствие trailing JSON: проверяйте контракт. Числа в interface{} становятся float64, если не включить UseNumber. Custom marshaler применяют для стабильного формата/инвариантов, избегая рекурсии через alias type. Не сериализуйте внутренние сущности напрямую, если API должен жить независимо от схемы.
Ключевые понятия
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Roles []string `json:"roles,omitempty"`
Token string `json:"-"`
}
func decodeUser(r io.Reader) (User, error) {
var u User
d := json.NewDecoder(r)
d.DisallowUnknownFields()
if err := d.Decode(&u); err != nil { return User{}, err }
if u.ID == "" { return User{}, errors.New("id is required") }
return u, nil
}
type Status uint8
const (New Status = iota; Done)
func (s Status) MarshalJSON() ([]byte, error) {
if s > Done { return nil, fmt.Errorf("invalid status: %d", s) }
return json.Marshal(map[Status]string{New: "new", Done: "done"}[s])
}
func (s *Status) UnmarshalJSON(b []byte) error {
var text string
if err := json.Unmarshal(b, &text); err != nil { return err }
switch text { case "new": *s = New; case "done": *s = Done; default: return fmt.Errorf("invalid status %q", text) }
return nil
}omitempty убирает zero value: 0, false, "", nil pointer/interface и nil либо пустой slice/map. Если «отсутствует» и «пустой» различаются для API, моделируйте это отдельно (например, pointer или custom type).
Типовые вопросы
- Почему поле не попало в JSON?
- Непубличное поле игнорируется; также его мог исключить тег
json:"-".
- Непубличное поле игнорируется; также его мог исключить тег
- Что делает
omitempty?- Не выводит поле с пустым значением по правилам пакета.
- Как отклонить неизвестные поля?
Decoder.DisallowUnknownFields()доDecode.
- Когда писать
MarshalJSONиUnmarshalJSON?- Когда внешний формат или инвариант не выражается тегами; реализуйте обе стороны, если тип также принимает JSON, и валидируйте вход в
UnmarshalJSON.
- Когда внешний формат или инвариант не выражается тегами; реализуйте обе стороны, если тип также принимает JSON, и валидируйте вход в
- Почему нельзя игнорировать ошибку Encoder?
- Запись в
io.Writerможет не выполниться; ответ клиенту окажется неполным.
- Запись в
Практика
- Опишите request DTO с обязательным
idи скрытым секретом. Готово: unit-тесты проверяют tag, unknown field, пустой id и отсутствие секрета. - Реализуйте enum с custom JSON. Готово: неизвестное значение не сериализуется, а round-trip допустимых значений проходит.
Частые ошибки и ловушки
- Полагаться на default field names как на долговечный публичный контракт.
- Путать
null,[]и отсутствующее поле. - Читать unbounded JSON body без ограничения размера на HTTP-границе.
Связанные темы
Коллекции и работа с данными · HTTP и Web