Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 21 из 25 ~24 мин +60 XP

JSON: кодирование и декодирование

Запрос «json go» — самая частая дорога в стандартную библиотеку после строк и файлов: данные нужно отдать наружу и принять обратно, и JSON — язык этого обмена. Двадцать первый урок разбирает пару «marshal unmarshal» целиком: json.Marshal превращает структуру в байты, json.Unmarshal возвращает байты в значения, struct tags решают, как поля зовутся в тексте и когда исчезают (omitempty), вложенные структуры, срезы и карты собирают формы любой глубины. Отдельно — честные мелочи, на которых спотыкаются: Marshal возвращает []byte, а не строку; кириллица выходит как есть, но <, > и & прячутся в \u003c-эскейпы, и отключаются SetEscapeHTML; json.Decoder читает поток значений вместо одного куска; json.RawMessage откладывает разбор до лучших времён. Мост из прошлого урока прямой: os.ReadFile дал байты — Unmarshal даст форму.

Содержание урока · 14 разделов

JSON в Go: байты, у которых есть форма#

JSON — JavaScript Object Notation — текстовый формат обмена данными: объекты в фигурных скобках, пары «ключ: значение», массивы в квадратных, строки в кавычках, числа, true/false и null. Но для Go важнее другое: JSON — это БАЙТЫ. Обычные байты UTF-8, о которых мы говорили в прошлом уроке, просто с договорённостью о том, что они значат: где кончается ключ, где начинается значение, что считать массивом. Прошлый урок дал вам os.ReadFile и контракт io.Reader — доставку байтов; этот урок даёт форму — упаковку значений в байты и обратно. Запрос «json go» на деле распадается на три навыка: кодировать значение в JSON (json.Marshal), декодировать JSON в значение (json.Unmarshal) и управлять формой текста (struct tags, omitempty, Encoder, Decoder). Этим трём навыкам и посвящён урок.

Почему вообще нужен формат вроде JSON? Срезы и структуры живут в памяти процесса и умирают вместе с ним; передать их по сети или сохранить на диск «как есть» нельзя — память процесса не транспорт. Нужна последовательность байтов, по которой получатель соберёт ту же структуру. Это называется сериализацией: упаковка значения в байтовую последовательность. Десериализация — обратная поездка: байты в значение. В Go за обе поездки отвечает один пакет — encoding/json, а маршалером и анмаршалером называют именно его функции: marshal — кодировать, unmarshal — декодировать; «marshal unmarshal» — пара, которую вы будете гуглить всю карьеру, и она же — ключевые слова этого урока.

Договоримся о честности песочницы сразу, как в прошлом уроке. Резервный движок знает только fmt, strings, strconv, math, sort, errors и time — encoding/json в этом списке нет, и мы зафиксируем точный текст отказа. Основной движок (yaegi) исполняет пакет честно: json.Marshal и json.Unmarshal с тегами, omitempty, Encoder и Decoder, RawMessage — всё работает, вывод детерминирован, каждый раннер урока прогнан трижды. А тот единственный полигон, который доступен обоим движкам, построен руками: JSON — просто текст, и его можно собрать из строки без всяких пакетов, чем мы и займёмся в первом раннере.

JSON руками: это просто текст

Раннер двойной: здесь нет encoding/json — только fmt, поэтому код исполняют ОБА движка. Запустите: на выходе почти JSON — {"name" : "Иван", "age" : 33} — и 33 байта (кириллица по два байта на букву, это байтовое знание прошлого урока). Эксперимент: уберите пробелы вокруг двоеточия в kv — текст станет компактнее, но останется тем же JSON: пробелы между парами не значимы. Попробуйте вставить в name кавычку через name := "И\"ван" — текст сломается: кавычка внутри JSON-строки обязана быть экранирована, и именно такие мелочи Marshal делает за вас всегда.

Вывод

Запустите код, чтобы увидеть вывод.

json.Marshal и struct tags: имя ключа решает тег#

json.Marshal(v) принимает значение любого типа и возвращает пару (b []byte, err error). Первое, что надо запомнить: результат — СРЕЗ БАЙТОВ, а не строка; JSON у нас байтовый, помните первый блок? Чтобы увидеть текст, нужна конверсия string(b) — та же, что у os.ReadFile в прошлом уроке. Второе: err обязателен к проверке. Marshal почти не ошибается на обычных данных (структуры, срезы, карты, числа, строки), но падает на том, что не умеет представить: канал, функция, комплексное число — для них вернётся UnsupportedTypeError, и молча проглоченная ошибка оставит вас с nil-срезом.

Для структур порядок полей и имена ключей решают struct tags — маленькие аннотации после объявления поля. Тег json:"name" говорит: «в JSON это поле зовётся name, не Name». Без тега Marshal использует имя поля как есть — с большой буквы; поэтому теги ставят почти всегда: API-контракты живут в нижнем регистре, а Go требует, чтобы поля были ЭКСПОРТИРУЕМЫМИ (с большой буквы) — неэкспортируемое поле Marshal молча пропустит, в JSON оно не попадает никогда, ведь пакет не может читать чужие приватные данные за пределами своего пакета. Два специальных значения тега: минус json:"-" полностью исключает поле из вывода, а через запятую идут опции — json:"name,omitempty" значит «зовётся name и прячется, когда пусто».

Теги читает рефлексия — reflect, десятый и восемнадцатый уроки курса уже дали вам её инструменты: Marshal не знает ничего о вашем типе на этапе компиляции, он в рантайме спрашивает у reflect.TypeOf поля, их теги и значения. Это же объясняет, почему тег — не комментарий: опечатка в json:\"nane\" не сломает компиляцию, поле просто выйдет под именем Name, и найти такой баг без теста непросто. И почему теги обязательны в виде строкового литерала: они разбираются парсером reflection-пакета по строгому формату имя,опция1,опция2.

Как Marshal кодирует базовые типы: строки — в кавычках (с нюансами, о которых ниже), числа — как есть, bool — true/false, nil-указатель и nil-интерфейс — null, срезы и массивы — массивы, карты — объекты, структуры — объекты с полями. Две неочевидные детали: срез байтов []byte кодируется СТРОКОЙ base64 ([]byte("hi") превратится в "aGk=") — это способ перевезти бинарные данные в текстовом формате; а ключи карты выводятся В ОТСОРТИРОВАННОМ порядке —.Marshal сортирует их, чтобы одинаковые данные давали одинаковые байты, что критично для кешей и подписей.

Marshal структуры с тегами

Раннер нативный: encoding/json исполняет только основной движок — резервный ответит отказом, текст которого честно записан в блоке о песочнице ниже. Запустите: первая строка — ошибка: <nil> байт: 46, затем {"email":"iv@example.ru","tags":["go","json"]} — пустые Name и Age спрятал omitempty. Второй вывод {"name":"Анна","email":""}: поле без omitempty выходит всегда, даже пустым. Третий вывод — числа [51 51]: json.Marshal вернул []byte, и Println напечатал значения байтов (51 — код символа '3'), а не текст; конверсия string(b) — обязательный последний шаг. Эксперимент: поменяйте тег Email на json:"-" — поле исчезнет из вывода целиком.

Вывод

Запустите код, чтобы увидеть вывод.

Проверьте себя

b, err := json.Marshal(v) отработал без ошибки. Как получить JSON-текст из b?

omitempty: нулевое значение — нет поля#

Опция omitempty в теге — самый частый гость после имени: json:"email,omitempty". Правило простое: если поле имеет НУЛЕВОЕ значение своего типа, оно не попадает в JSON вообще. Список нулевых значений стоит выучить: false для bool, 0 для любых чисел, "" для строк, nil для указателей, интерфейсов, срезов и карт (важно: nil-срез и ПУСТОЙ срез make([]T, 0) — не одно и то же для Marshal без omitempty — первый выйдет null, второй []; с omitempty спрячутся оба). Ключевое исключение, о которое спотыкаются все: omitempty НЕ РАБОТАЕТ для структур. Структура с нулевыми полями — не «нулевое значение» в смысле encoding/json, и поле-структура выйдет целиком со всеми нулями внутри. Хотите прятать опциональную структуру — делайте её указателем: nil-указатель omitempty прячет, а значение — выводит.

Зачем это нужно? Чтобы контракт ответа был предсказуемым и компактным. Поле «опции не заданы» в JSON с omitempty просто отсутствует — клиент по наличию ключа понимает, было ли значение; без omitempty пришлось бы везде таскать "status": "" и "count": 0, различая «пусто» и «не прислали» по соглашениям вместо фактов. Обратная сторона: ноль — иногда валидное значение. Поле Температура со значением 0 и omitempty исчезнет из ответа, и получатель не отличит «ноль градусов» от «датчик не отвечал» — в таких случаях omitempty не ставят или переходят на указатель.

Где вы уже это видели: в курсе Gin урок про JSON-ответы — там omitempty-форма собиралась для HTTP-хендлеров, и правило «структуры не прячутся, указатели — да» разбиралось на Todo-списке. Здесь, в стандартной библиотеке, механика та же до последней буквы: c.JSON внутри зовёт ровно тот json.Marshal, который вы вызываете руками. Связка полезная: научившись здесь, вы уже знаете, почему gin-ответ выглядит именно так.

Проверьте себя

Структура с полями Done bool `json:"done,omitempty"`, Note string `json:"note,omitempty"` и Children []string `json:"children,omitempty"`. Маршалим значение с Done: false, Note: "", Children: nil. Что выйдет?

json.Unmarshal: из байтов в значения#

Обратная поездка — json.Unmarshal(data []byte, v any) error. Два правила, на которых держится вся функция. Первое: v передаётся ПО АДРЕСУ — Unmarshal должен записать результат в вашу переменную, поэтому вызов всегда выглядит как err := json.Unmarshal(b, &p); забыли амперсанд — компилятор пропустит (any принимает всё), но поля останутся нулями: функция заполнила копию, которую тут же выбросила. Второе: data — это []byte, и здесь мост из прошлого урока замыкается: os.ReadFile вернул data []byte из файла конфигурации — Unmarshal превращает эти байты в структуру; это самая частая связка в реальных программах, и она стоит двух строк.

Сверка имён: Unmarshal идёт по JSON-ключам и ищет совпадающее поле адресата — сначала точное совпадение с тегом, потом с именем поля. JSON-ключ, для которого поля нет, МОЛЧА ИГНОРИРУЕТСЯ — это осознанное решение encoding/json: API добавляет новые поля, старые клиенты не падают. Но и опечатка в ожидаемом поле молча даст нулевое значение — строгую проверку формы делают отдельными библиотеками или через Decoder с DisallowUnknownFields, о котором поговорим в блоке про поток. Поле, отсутствующее в JSON, остаётся с нулевым значением — Unmarshal никогда не обнуляет то, что вы уже записали в структуру до вызова: он обновляет только то, что нашёл.

Типы при декодировании: JSON-число ложится в int, float64 или любой числовой тип поля — Unmarshal приведёт значение к типу поля (и вернёт ошибку UnmarshalTypeError, если «порт»: "восемьдесят" не лезет в int). JSON true/false — в bool, null — в nil для указателей, срезов, карт и интерфейсов (в число null не декодируется — останется нулевым). Битые байты — не-JSON вроде {broken — дают SyntaxError с позицией; ошибку проверяют всегда, nil-структура после провала разбора — классическая тихая беда.

Unmarshal: структура и map[string]any

Раннер нативный: запускайте на основном движке. Первый вывод — ошибка: <nil> сумма: 7: поле z без пары в структуре проигнорировано, x и y легли по тегам. Второй блок — map[string]any: кириллические ключи работают как любые другие, значения вышли через индексирование карты. Обратите внимание: nums[1] печатается как 2, но по типу это float64 — числа в any после Unmarshal всегда float64, и арифметика с ними требует приведения. Эксперимент: уберите амперсанд перед p — поля останутся нулями, сумма станет 0, а ошибка — nil: самый тихий баг урока.

Вывод

Запустите код, чтобы увидеть вывод.

Что выйдет из Marshal с omitempty?

Прочитайте код и выберите, что он выведет:

package main

import (
        "encoding/json"
        "fmt"
)

// Todo — форма задачи: omitempty прячет нулевые поля.
type Todo struct {
        Title    string   `json:"title"`
        Done     bool     `json:"done,omitempty"`
        Priority int      `json:"priority,omitempty"`
        Note     string   `json:"note,omitempty"`
}

func main() {
        b, _ := json.Marshal(Todo{Title: "выучить tags"})
        fmt.Println(string(b))
}

Вложенные структуры, срезы и карты#

Плоские формы скучны, и к счастью, Marshal/Unmarshal рекурсивны: поле-структура кодируется вложенным объектом, срез структур — массивом объектов, карта со структурами — объектом, у которого ключи карты стали ключами JSON. Правило одно: форма в JSON повторяет форму значения, на любую глубину. Теги работают на каждом уровне: у вложенной структуры — свои теги, и их имена видны в конечном тексте. Единственное ограничение — циклы: структура, которая ссылается сама на себя через значение, не кодируется (Marshal вернёт ошибку о неподдерживаемом значении); циклы через указатели тоже не разрулить — рекурсия должна кончаться в данных, а не в типе.

Срез структур — рабочая лошадка API: []Person с вложенными Address маршалится в массив объектов, и Unmarshal так же честно соберёт срез обратно, выделив память под каждый элемент сам. Карта — способ выразить «ключи заранее неизвестны»: map[string]int выйдет объектом с числами-значениями, и наоборот, объект с произвольными ключами удобно декодировать в карту, а не в структуру. Помните про сортировку: ключи карты в выводе всегда отсортированы — Marshal не оставит вам порядок вставки, как у срезов.

Выбор между структурой и картой — выбор между контрактом и гибкостью. Структура: поля известны, имена и типы проверяет компилятор, теги дают документацию; неправильные типы JSON ловятся в момент Unmarshal. Карта: форма динамическая — конфиг с произвольными секциями, результат парсера, «передай что угодно»; типы проверять приходится вручную через утверждения типов, а числа в any живут как float64. Зрелый код часто комбинирует: верхний уровень — структура с известными полями, одно из полей — map[string]any или json.RawMessage для части, чья форма меняется.

Соберите Marshal-программу

Соберите программу: тип с тегом, значение, json.Marshal и печать текста с проверкой ошибки. Строки импорта — по одной на пакет.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Восстановите вызов Unmarshal

Допишите декодирование: имя функции пакета и адрес переменной, в которую ляжет результат. Ответы вводите латиницей, второй — с амперсандом, без пробелов.

package main import ( "encoding/json" "fmt" ) // Point — адресат разбора: ключи сверяются с тегами. type Point struct { X int `json:"x"` Y int `json:"y"` } func main() { var p Point // Разбираем байты в структуру: адрес обязателен. err := json.([]byte(`{"x": 7, "y": 2}`), ) fmt.Println(err == nil, p.X+p.Y) }

Кириллица и \u003c: HTML-эскейпы и SetEscapeHTML#

Что Marshal делает с не-ASCII текстом? Ничего: кириллица, эмодзи и любой валидный UTF-8 выходят КАК ЕСТЬ — сырыми байтами. json.Marshal("привет") даст четырнадцать байт с кавычками, где сами буквы — те самые по два байта из прошлого урока; len(string(b)) посчитает их честно. Юникод-эскейпы вида \u043f Marshal для кириллицы не генерирует — если вы их видели в ответах API, это либо другой язык, либо ручная обработка. Отсюда практическое правило: JSON с кириллицей весит больше, чем «выглядит», — счёт ведётся в байтах, и Content-Length вычисляется по len(b), а не по количеству знаков.

А вот три символа — <, > и & — Marshal экранирует ВСЕГДА по умолчанию: < станет \u003c, > — \u003e, & — \u0026. Зачем: если JSON вклеивается в HTML-страницу сырыми байтами (шаблонизаторы, inline-скрипты), строка без экранирования могла бы вынести за пределы JSON-литерала тег <script> — это вектор атаки под названием XSS. Экранирование не меняет смысла данных: любой JSON-парсер развернёт \u003c обратно в <. Для обычных API это не мешает вообще.

Когда байты важны — например, вы считаете хеш тела для кеша или клиенту нужен точный текст — экранирование отключают, и делается это не функцией Marshal, а Encoder'ом: enc := json.NewEncoder(w) создаёт энкодер, пишущий JSON прямо в Writer w; метод enc.SetEscapeHTML(false) выключает HTML-эскейпы, а enc.Encode(v) кодирует значение и дописывает в конец перевод строки — единственная байтовая разница помимо экранирования. В Gin вы это уже видели: c.PureJSON — это именно Encoder с SetEscapeHTML(false) против обычного c.JSON; теперь вы знаете, что под капотом. Метод Encode принимает любой io.Writer — буфер в памяти, файл из прошлого урока, сетевое соединение: контракт Reader/Writer снова экономит целый слой кода.

Encoder, SetEscapeHTML и точные байты

Раннер нативный: encoding/json и strings.Builder исполняет основной движок. Запустите: marshal-строка несёт \u003cb\u003e\u0026 — три экранированных символа, encoder-строка — честные <b>&. Внимание на байты: буфер держит 24 — фигурные скобки и двоеточие, кавычки, "шаблон" по два байта на шесть букв (двенадцать плюс кавычки), значение <b>& из четырёх знаков и ДОПИСАННЫЙ Encode'ом перевод строки: вес ответа считается в байтах, не в знаках. Эксперимент: уберите SetEscapeHTML(false) — обе строки совпадут с экранированием, а длина вырастет на пятнадцать байт: по пять на каждый \u00XX вместо одиночного символа.

Вывод

Запустите код, чтобы увидеть вывод.

json.Decoder против Unmarshal: поток#

json.Unmarshal работает с готовым []byte: весь JSON должен лежать в памяти до вызова. json.Decoder — второй способ декодирования: dec := json.NewDecoder(r) принимает ЛЮБОГО io.Reader и читает значение МЕТОДОМ dec.Decode(&v) — по одному за вызов, подтягивая байты из источника по мере необходимости. Зачем второй способ? Три причины. Первая — поток из нескольких значений: источники вроде NDJSON (по одному JSON-объекту на строку) и JSON-стримов отдают НЕ один документ, а конвейер документов подряд — {"n":1}{"n":2}: Unmarshal съест только первый и подавится вторым, а Decoder прожуёт их циклом: for { err := dec.Decode(&v); if err != nil { break } } — выход из цикла по ошибке, которая в конце потока и есть io.EOF, штатный сигнал конца из прошлого урока.

Вторая причина — память и латентность: Decoder не держит весь документ целиком, он читает ровно столько байтов, сколько нужно для следующего значения; гигабайтный лог JSON-строками можно обрабатывать построчно-потоково, не грузя файл в память, — в связке с bufio.Scanner из прошлого урока это стандартный жанр обработки больших выгрузок. Третья — настройки: у Decoder есть методы вроде dec.DisallowUnknownFields(), превращающий молчаливое игнорирование незнакомых полей в ошибку, — строгий режим для контрактов, где опечатка в ключе должна быть громкой, а не тихой.

Когда какой способ выбирать — правило простое. Один JSON-объект, уже лежащий в байтах (os.ReadFile, []byte из ответа, который вы всё равно прочитали целиком) — Unmarshal: одна строка, ноль церемоний. Поток из многих значений, источник без конца (сеть), документ слишком велик для памяти или нужны строгие настройки — Decoder. У Encoder/Decoder есть и младший брат: json.MarshalIndent(v, "", " ") — тот же Marshal с отступами для людей: логи, диффы, отладочные дампы. Для машинных ответов отступы — просто лишние байты.

json.RawMessage: разбери потом#

Последний инструмент урока — json.RawMessage. По определению это просто синоним []byte, но с двумя суперспособностями: при МАРШАЛИНГЕ RawMessage вставляется в вывод КАК ЕСТЬ, сырыми байтами, без повторного кодирования; при ДЕКОДИРОВАНИИ RawMessage забирает кусок JSON целиком, не разбирая его. Классический паттерн — конверт: обёртка с известными полями kind и body, где форма body зависит от kind. Структура объявляет Body json.RawMessage, Unmarshal разбирает только конверт, а вы, посмотрев на e.Kind, отправляете e.Body на второй Unmarshal — уже в конкретный тип. Один разбор — два прохода, каждый со своей формой.

Зачем не map[string]any? Карта разбирает ВСЁ сразу и прячет типы за float64 и утверждениями; RawMessage сохраняет точные байты — их можно передать дальше, положить в другую структуру, залогировать дословно или разобрать лениво, только когда ветка кода действительно понадобилась (не разбирали — не платили). Это техника «отложенного решения», и в больших системах она экономит и время, и нервы: парсер конвертов не знает о тысяче видов body, а каждый обработчик знает только свою.

Граница применимости честная: RawMessage — инструмент для ДВУХЭТАПНОГО разбора и прозрачной пересылки. Если форма известна заранее — структура проще и надёжнее; если форма неведома и нужна возня с полями — карта честнее. RawMessage занимает нишу между ними: форма известна ПОЗЖЕ, а не никогда. И маленькая деталь на память: RawMessage в структуре с omitempty не прячется — nil-срез выйдет как null; если пустой конверт должен быть без body, чистите поле руками или используйте указатель *json.RawMessage.

Шпаргалка: JSON из этого урока#

ИнструментЧто делаетКак выглядитОсторожно
json.Marshalзначение → []byte с JSONb, err := json.Marshal(v)вернёт []byte: текст — string(b)
json.Unmarshal[]byte → значение по адресуerr := json.Unmarshal(b, &v)без & поля останутся нулями
struct tagsимя ключа и опции поляjson:"name,omitempty"опечатка в теге не ловится компилятором
omitemptyспрятать нулевое полеjson:"email,omitempty"false/0/""/nil/пустые slice+map; структуры не прячет
json:"-"исключить поле из JSONToken string json:"-"неэкспортируемые поля и так молча пропускаются
map[string]anyдинамический разборvar m map[string]any; m["k"]числа float64; типы — утверждениями
json.Encoderпотоковая запись в Writerenc := json.NewEncoder(w)Encode дописывает перевод строки
SetEscapeHTML(false)отключить \u003c-эскейпыenc.SetEscapeHTML(false)по умолчанию < > & экранируются всегда
json.Decoderпотоковое чтение из Readerdec := json.NewDecoder(r)цикл Decode до io.EOF; DisallowUnknownFields
json.MarshalIndentтот же Marshal с отступамиjson.MarshalIndent(v, "", " ")для людей: логи и диффы, не для API-ответов
json.RawMessageсырые байты: разобрать потомBody json.RawMessageмаршалится как есть; nil выходит null
[]byte в JSONсрез байтов кодируется base64json.Marshal([]byte("hi"))даст "aGk=": так возят бинарные данные

Правила гигиены JSON#

  • Marshal возвращает []byte: текст — string(b); Println(b) напечатает числа, а не JSON.
  • Проверяйте err у обоих вызовов: Marshal на непредставимых значениях (канал, функция) вернёт ошибку, Unmarshal — на битых байтах и неверных типах.
  • Unmarshal зовите с адресом: &p, а не p — без амперсанда функция заполнит копию, и поля молча останутся нулями.
  • Неизвестные JSON-ключи Unmarshal игнорирует молча: это совместимость, но и укрытие для опечаток — строгий режим включается Decoder'ом с DisallowUnknownFields.
  • omitempty прячет false, 0, "", nil и пустые срезы с картами — но НИКОГДА не прячет структуры: опциональная структура — это указатель.
  • Пустой срез и nil-срез — разные байты: make([]T, 0) выйдет [], nil выйдет null; для списков инициализируйте срез даже пустым.
  • Кириллица в JSON — сырые байты UTF-8, счёт веса в байтах; <, > и & по умолчанию экранируются \u003c, \u003e и \u0026.
  • Точные байты тела — Encoder с SetEscapeHTML(false) (так работает PureJSON в Gin); Encode дописывает перевод строки в конец.
  • Ключи карт в выводе отсортированы: одинаковые данные дают одинаковые байты — на этом держатся кеши и подписи.
  • Поток из многих JSON-документов — Decoder и цикл Decode до io.EOF; один готовый []byte — Unmarshal.
  • Форма известна позже, а не никогда — json.RawMessage: конверт разбираем, body оставляем байтами до второго прохода.

Что дальше: JSON выходит в сеть#

Соберём урок в одну карту. Кодирование: json.Marshal — значение в []byte, теги называют ключи, omitempty прячет нулевые (но не структуры), неэкспортируемое поле не выходит вовсе, ключи карт сортированы, []byte превращается в base64. Декодирование: json.Unmarshal — байты в значение по адресу, незнакомые ключи молча мимо, типы приводятся к типам полей, а в map[string]any всё живёт динамикой с float64-числами. Поток: Encoder пишет в Writer (SetEscapeHTML(false) для точных байтов, \n в конце), Decoder читает из Reader по одному значению до io.EOF. Отложенный разбор: json.RawMessage хранит сырые байты между двумя проходами. И весь урок держится на прошлом: JSON — байты UTF-8, os.ReadFile их доставляет, контракт io.Reader делает их потоком.

Двадцать второй урок — HTTP-клиент: http.Get, http.Client, таймауты и чтение ответа. Связка самая прямая в курсе: тело ответа — это io.Reader, и живой жанр «запросил API — декодируй ответ» собирается из двух уроков подряд: resp.Body отдаёт байты, json.Decoder или Unmarshal превращает их в структуры. Вы уже умеете вторую половину; скоро появится первая. А если хочется увидеть JSON в роли сервера — урок про JSON-ответы в Gin показывает ту же encoding/json с другой стороны очереди: теги, omitempty и PureJSON на настоящих хендлерах.

До встречи на странице курса. Если захочется освежить байтовую базу — урок про файлы и ввод-вывод: os.ReadFile, поток и EOF — то, во что JSON упирается с обеих сторон. А если станет интересно, как Marshal вообще видит ваши поля и теги — урок про рефлексию: reflect.TypeOf, reflect.Value и StructTag — три инструмента, из которых encoding/json собран внутри.

Карточки урока

Термин

json.Marshal: что и во что кодирует

нажмите, чтобы перевернуть
Определение

json.Marshal(v) — значение любого типа в []byte с JSON (err второй парой; проверять). Текст — string(b), Println(b) печатает числа байтов. Поля структур — только экспортируемые; тег json:"имя" переименовывает ключ, json:"-" исключает поле, omitempty прячет нулевое. Ключи карт выводятся отсортированными; []byte кодируется строкой base64; nil-указатели и nil-интерфейсы — null; срезы и массивы — массивы.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • JSON — байты UTF-8 с договорённостью о форме: кодирование — json.Marshal (значение → []byte), декодирование — json.Unmarshal ([]byte → значение); пара «marshal unmarshal» — две половины одной сериализации.
  • json.Marshal возвращает []byte и err: текст — string(b); поля структур — только экспортируемые, тег json:"имя" переименовывает ключ, json:"-" исключает поле.
  • omitempty прячет нулевые значения — false, 0, "", nil-указатель, nil-срез, nil-карту; структуры не прячет никогда: опциональная структура — это указатель.
  • json.Unmarshal зовётся с адресом (&p): без амперсанда заполнится копия и поля молча останутся нулями; неизвестные ключи игнорируются, отсутствующие оставляют нули.
  • map[string]any — динамический разбор любой формы: числа живут как float64, значения достаются утверждениями типов; структуры — контракт, проверяемый компилятором и Unmarshal'ом.
  • Вложенные структуры, срезы структур и карты кодируются рекурсивно: форма JSON повторяет форму значения; ключи карт в выводе всегда отсортированы.
  • Кириллица выходит сырыми байтами UTF-8 без эскейпов; <, > и & по умолчанию экранируются \u003c, \u003e, \u0026 — защита от XSS при вклейке в HTML.
  • Точные байты — json.Encoder с SetEscapeHTML(false): Encode пишет в любой Writer и дописывает перевод строки; так устроен PureJSON в Gin.
  • json.Decoder читает поток из любого io.Reader: Decode в цикле прожёвывает несколько JSON-документов подряд до io.EOF; Unmarshal — для одного готового []byte.
  • json.RawMessage хранит сырые байты между двумя проходами: конверт разбираем сразу, body — по kind и по потребности.
  • Песочница честна: encoding/json в фолбэке нет (точный текст отказа зафиксирован в уроке), нативный движок исполняет Marshal, Unmarshal, Encoder, Decoder и RawMessage детерминированно.

Домашнее задание

Часть первая — Marshal: опишите структуру Книга (Название, Автор, Год, Подзаголовок с omitempty) и выведите Marshal'ом три значения: полностью заполненное, с пустым Подзаголовком и с нулевым Годом; в комментарии объясните, какие ключи и почему исчезли, и посчитайте len(b) для кириллического названия, сверив с числом букв. Часть вторая — Unmarshal: разберите байты {"город": "Уфа", "население": 1125000} в map[string]any, напечатайте оба значения и тип числа через fmt.Printf("%T"); затем опишите структуру под этот же JSON и разберите в неё — в комментарии сравните два подхода. Часть третья — эскейпы: маршальте map со строкой "a<b&c" двумя способами (Marshal и Encoder с SetEscapeHTML(false)), посчитайте len обоих результатов и объясните разницу в шести байтах. Часть четвёртая — Decoder: соберите строку из двух JSON-объектов подряд, прочитайте её json.NewDecoder(strings.NewReader(...)) в цикле с подсчётом объектов и напечатайте причину остановки; затем попробуйте то же Unmarshal'ом и опишите, что он съел. Комментарии в коде — по-русски.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Pro · JSON: кодирование и декодирование)

Вопросы и ответы по уроку

Почему json.Marshal возвращает []byte, а не string?

Потому что JSON в Go — это в первую очередь байты, готовые к отправке: http.ResponseWriter, os.File и сетевые соединения принимают именно []byte, и лишняя конверсия в строку была бы лишним копированием в самом горячем месте программы. Строка нужна только тогда, когда вы дальше работаете с текстом руками (лог, сравнение, склейка) — тогда делаете string(b) явно, как string(data) у os.ReadFile. Косвенный бонус: тип результата напоминает, что len считает байты, а не знаки — для кириллических JSON это критично при выставлении Content-Length.

Почему поле с omitempty всё равно попадает в JSON?

Проверьте два типа ловушек. Первая: omitempty прячет только НУЛЕВЫЕ значения своего типа — false, 0, "", nil; любая непустая строка, ненулевое число и даже отрицательное значение выводятся. Вторая: omitempty не работает для полей-структур — структура с нулевыми полями не считается нулевым значением и выходит целиком; чтобы прятать, объявите поле указателем (*Address): nil-указатель спрячется, значение — выйдет. И не забудьте, что тег должен начинаться с json: и опция пишется через запятую после имени: `json:"email,omitempty"`.

Чем json.Decoder лучше json.Unmarshal?

Это не «лучше/хуже», а разные источники. Unmarshal берёт готовый []byte — весь JSON уже в памяти, один вызов. Decoder обёрнут вокруг io.Reader и читает байты по мере надобности: разбирает ПОТОК из нескольких JSON-документов подряд (цикл Decode до io.EOF), не требует весь документ в памяти разом и даёт настройки вроде DisallowUnknownFields. Один маленький объект из os.ReadFile — Unmarshal; NDJSON-лог на гигабайт, бесконечный стрим по сети или строгий контракт — Decoder. Механика разбора у них одна и та же.

Работает ли encoding/json в песочнице самоучителя?

Только на основном движке. Резервный знает лишь fmt, strings, strconv, math, sort, errors и time — import "encoding/json" даёт отказ «пакет "encoding/json" недоступен в песочнице (доступны: fmt, strings, strconv, math, sort, errors, time)». Основной движок (yaegi) исполняет пакет честно: Marshal и Unmarshal с тегами и omitempty, Encoder с SetEscapeHTML, Decoder с потоком, RawMessage и MarshalIndent — все JSON-раннеры урока нативные и прогнаны трижды. Форму JSON можно изучить и без пакета: первый раннер собирает JSON-текст руками из строки, и такой код исполняют оба движка.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией