Marshal и Unmarshal
json.Marshal превращает значение Go в байты JSON. json.Unmarshal заполняет значение Go из байтов JSON. Теги структур выбирают имена полей JSON.
Email нет в первом выводе из-за omitempty и пустой строки. Unmarshal нужен указатель (&back); если передать само значение, вернётся InvalidUnmarshalError.
Теги структур
Синтаксис тега: json:"name,option,option".
| Тег | Эффект |
|---|---|
json:"user_id" | использовать ключ user_id |
json:"email,omitempty" | не выводить, если false, 0, "", nil, пустой слайс или мапа |
json:",omitzero" (Go 1.24) | не выводить, если значение нулевое или его IsZero() возвращает true |
json:"-" | никогда не кодировать и не декодировать это поле |
json:"-," | использовать буквальный ключ - |
json:"count,string" | кодировать число или bool как строку JSON ("42") |
| без тега | ключ совпадает с именем поля Go, UserID |
Пароль никогда не покидает структуру, баланс выводится в кавычках, а deleted_at пропадает, тогда как created_at печатает нулевое время 0001-01-01T00:00:00Z. Именно из-за этой разницы и добавили omitzero: omitempty никогда не работал для структур, и с time.Time это давний сюрприз. На Go 1.23 и раньше используйте *time.Time с omitempty, чтобы получить тот же эффект.
MarshalIndent(v, prefix, indent) даёт читаемый вывод. Используйте его для файлов конфигурации и отладки; API обычно отправляют компактный JSON.
Только экспортированные поля
encoding/json использует рефлексию и видит только экспортированные поля. Это самый частый баг с JSON в Go:
type point struct {
x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}
Ни ошибки, ни предупреждения, просто {} на выходе и поля, оставшиеся нулевыми, на входе. Пишите имена полей с заглавной буквы и добавляйте теги для ключей в нижнем регистре.
Как декодирование сопоставляет поля
При декодировании в структуру:
- Ключи сопоставляются с именем из тега или с именем поля без учёта регистра.
{"NAME": "x"}заполнит поле с тегомjson:"name". - Ключи без подходящего поля молча игнорируются.
- Поля без подходящего ключа сохраняют текущее значение. Unmarshal их не сбрасывает, поэтому декодирование в структуру, где уже есть данные, сливается с ними.
- Несовпадение типов (строка там, где в структуре
int) возвращает*json.UnmarshalTypeError, но остальные поля всё равно заполняются.
Чтобы отвергать неожиданные ключи, например в строгом API или в файле конфигурации с опечатками, используйте Decoder с DisallowUnknownFields:
Неизвестная структура: map[string]any
Когда форма данных заранее неизвестна, декодируйте в map[string]any или any. Соответствие фиксированное:
| JSON | Go |
|---|---|
| объект | map[string]any |
| массив | []any |
| строка | string |
| число | float64 |
| true / false | bool |
| null | nil |
В выводе видны две ловушки. Каждое число это float64, поэтому m["stock"].(int) не сработает. А целые больше 2^53 теряют точность как float64: id 9007199254740993 возвращается как ...992. UseNumber избавляет от обеих.
Если часть структуры известна, декодируйте эту часть в структуру, а для остального используйте json.RawMessage. Он хранит сырые байты поля, чтобы вы могли декодировать его позже, когда узнаете тип.
Слайсы, мапы, указатели и nil
| Значение Go | JSON |
|---|---|
nil-слайс (var s []int) | null |
пустой слайс ([]int{}) | [] |
nil-мапа | null |
nil-указатель | null |
[]byte | строка в base64 |
map[string]T | объект, ключи отсортированы |
map[int]T | объект с целыми ключами в виде строк |
Разница между nil и пустым слайсом важна для клиентов API, которые ждут массив. Инициализируйте через []T{} или make([]T, 0), когда поле должно быть [].
Используйте поле-указатель (*int, *bool), когда на входе нужно отличать «отсутствует» от «ноль». После декодирования nil-указатель означает, что ключа не было или он равен null; указатель на 0 означает, что клиент прислал 0.
Потоки: Encoder и Decoder
json.Marshal и Unmarshal работают со слайсами байтов целиком. Для io.Reader или io.Writer (тело HTTP, файл, stdin) используйте json.NewDecoder и json.NewEncoder. Decoder умеет также читать последовательность значений JSON по одному:
Encoder.Encode пишет перевод строки после каждого значения. По умолчанию и Marshal, и Encoder экранируют <, > и & как \u003c, \u003e и \u0026, чтобы JSON можно было безопасно вставлять в HTML. SetEscapeHTML(false) это отключает. В HTTP-обработчиках обычная пара это json.NewDecoder(r.Body).Decode(&v) и json.NewEncoder(w).Encode(v).
Собственное кодирование
Тип может сам управлять своим JSON, реализовав json.Marshaler и json.Unmarshaler. Частый случай: перечисление через iota, которое должно выглядеть как имя, а не число:
У MarshalJSON получатель-значение, поэтому он работает и для значений, и для указателей; UnmarshalJSON нужен получатель-указатель, потому что он меняет значение. Типы, которым нужна только строковая форма, могут вместо этого реализовать encoding.TextMarshaler (MarshalText), и тогда их можно использовать ещё и как ключи мап.
Частые ошибки
- Имена полей со строчной буквы. Они молча пропускаются.
- Передача в
Unmarshalзначения. Нужен указатель. - Игнорирование ошибки. О некорректном JSON и несовпадении типов сообщает только она.
- Расчёт на то, что числа в
map[string]anyэтоint. Ониfloat64. - Ожидание, что
omitemptyуберёт пустую структуру или нулевойtime.Time. Используйтеomitzeroна Go 1.24 или указатель. - Расчёт на порядок полей в мапах. Ключи мап на выходе сортируются; поля структур сохраняют порядок объявления.
Часто задаваемые вопросы
Как преобразовать структуру в JSON в Go?
Вызовите json.Marshal(v), которая возвращает []byte и ошибку. Попадают туда только экспортированные поля (имена с заглавной буквы). Чтобы задать ключ JSON, используйте на поле тег вроде json:"name". Для вывода с отступами используйте json.MarshalIndent(v, "", " ").
Почему поля моей структуры отсутствуют в JSON?
encoding/json видит только экспортированные поля. Поле с именем name (со строчной буквы) для него невидимо и при кодировании, и при декодировании. Напишите имя поля с заглавной буквы и задайте ключ JSON тегом вроде json:"name".
Что делает omitempty в JSON в Go?
omitempty не выводит поле, когда в нём пустое значение: false, 0, "", nil-указатель или интерфейс, пустой слайс или мапа. Структуру или time.Time он пустыми не считает. В Go 1.24 появился omitzero, который опускает любое значение, равное нулевому значению своего типа (или чей метод IsZero() возвращает true), включая структуры и time.Time.
Как разобрать JSON с неизвестной структурой в Go?
Декодируйте в map[string]any (или в any). Объекты становятся map[string]any, массивы []any, строки string, булевы значения bool, а любые числа float64. Читайте значения через утверждения типа, а если большие целые должны остаться точными, используйте Decoder.UseNumber.