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

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

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

Урок 5 из 25 ~18 мин +50 XP

Структуры и bson-теги

bson-теги — то место, где Go-структура превращается в документ mongodb: без них драйвер придумает имена полей сам, а bson omitempty молча выкинет нули из записи. Разберём дефолты кодека, каждый модификатор тега, словарь типов bson.D, bson.M и bson.E — и ошибки, которые встречаются чаще всего. Теги — это контракт с кодеком, и чем раньше вы его оформите явно, тем меньше сюрпризов в коллекции.

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

У вас наверняка уже есть структуры с тегами json — API-слой про них позаботился. Но документ в MongoDB кодируется в BSON, и за него отвечает отдельный тег bson. Между json и bson есть тонкое расхождение: JSON-кодек оставляет имена полей как есть, а BSON-кодек по умолчанию приводит их к нижнему регистру — и в документе внезапно оказывается httpcode вместо HTTPCode. Пропущенные bson-теги не ломают вставку, они ломают читаемость и согласованность: поле сегодня называется one way, а завтра вы пишете фильтр и не помните, как оно легло в коллекцию. В этом уроке возьмём кодек под контроль: имена, пустые значения, встраивание, исключение полей — всё через один тег с запятыми и опциями. Это последний урок фундамента: со следующего курса пойдёт чтение — FindOne и курсоры.

bson-теги: как драйвер называет поля#

Правило по умолчанию стоит запомнить дословно: без тега кодек драйвера пишет имя поля строчными буквами целиком. Name станет name, HTTPCode — httpcode, CreatedAt — createdat. Поэтому первый и главный модификатор — имя в теге: bson:"name" переопределяет ключ документа. Соглашение, которое я встречаю в Go-проектах чаще всего: snake_case — bson:"created_at", bson:"sku"; оно совпадает с традициями MongoDB-мира и с удобством чтения в mongosh. Порядок полей в структуре — тоже контракт: кодек пишет поля в порядке объявления, и BSON порядок сохраняет — это важно для _id, который обязан быть первым полем, и для читаемости документа глазами. Неэкспортируемые поля кодек пропускает молча — их в документе не будет никогда. И отдельно: если поле структуры вообще не должно попадать в базу, ставьте тег bson:"-" — минус явно исключает поле из кодирования и декодирования. Тег разбирается по запятой: первая часть — имя (пустая при ,inline), дальше — модификаторы. Анонимные вложенные структуры без тега кодек вливает автоматически — как неявный inline, поэтому композиция моделей работает и без лишних тегов.

Структура с полным набором тегов

GO
type Specs struct {
        Color string `bson:"color"`
        Size  string `bson:"size"`
}

type Product struct {
        ID       bson.ObjectID `bson:"_id,omitempty"`
        Name     string        `bson:"name"`
        Price    int64         `bson:"price,omitempty"`
        InStock  bool          `bson:"in_stock,omitempty"`
        Specs    Specs         `bson:",inline"`
        internal string        // неэкспортируемое — кодек пропустит
}

func main() {
        p := Product{Name: "Клавиатура", InStock: true, Specs: Specs{Color: "серый", Size: "TKL"}}

        data, err := bson.MarshalExtJSON(p, false, false)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(string(data))
        // {"name":"Клавиатура","in_stock":true,"color":"серый","size":"TKL"}
}

Без тегов драйвер написал бы name, price, instock — строчными целиком. Теги дают контроль: omitempty выбросил нулевую Price, а InStock: true прошёл. ,inline влил поля Specs на верхний уровень документа: color и size стали полями самого продукта. Неэкспортируемое поле internal в документ не попадает никогда. MarshalExtJSON показывает итоговый документ без запуска базы — удобная штука для отладки тегов.

Модификаторы bson-тега#

МодификаторЧто делаетПример поляЭффект в документе
имяпереименовывает полеbson:"created_at"ключ created_at вместо createdat
omitemptyпропускает пустые значенияbson:"price,omitempty"нулевая цена не попадает в документ
minsizeint64 кодируется как int32, если влезаетbson:"qty,minsize"поле занимает 4 байта вместо 8
inlineвливает поля вложенной структурыbson:",inline"color и size без уровня Specs
-исключает полеbson:"-"поля в документе нет вообще

bson omitempty: что считается пустым#

Модификатор omitempty — источник половины багов с тегами, поэтому разберём его буквально. Пустым кодек считает: false у bool, ноль у любых чисел, пустую строку, nil-указатель, nil-срез, nil-карту и пустую структуру времени. Не пустым — всё остальное: не-nil указатель на ноль, срез с элементами, false нельзя «сделать не пустым» без указателя. Отсюда практические следствия. Число-ноль как легитимное значение — цена со скидкой 0, количество 0 — нельзя декорировать omitempty: поле исчезнет, и при декодировании вернётся тот же ноль, а вот в фильтрах и валидации документ окажется «без цены». Для таких полей используйте указатель int64: nil — пусто, а указатель на ноль — осмысленный ноль, и omitempty сработает честно. Тот же приём спасает bool-флаги, где false и «поле не задано» — разные вещи. Вопрос «bson omitempty когда применять» решается одним тестом: может ли значение быть нулём легитимно? Может — указатель или без omitempty; ноль всегда означает «нет данных» — смело ставьте. Отдельная история — время: нулевое time.Time тоже считается пустым при omitempty, поэтому для полей вроде deleted_at используйте указатель time.Time: nil — метки нет, а не «первое января 1970 года».

bson.D, bson.M, bson.E: словарь типов#

Структуры — не единственный способ собрать документ, и в запросах вы постоянно будете видеть три типа из пакета bson. bson.E — пара {Key, Value}, кирпичик документа. bson.D — []E, упорядоченный документ: порядок полей именно такой, как вы написали; его выбирают для команд сервера и фильтров, где порядок операторов важен ($group с сортировкой, составные ключи). bson.M — map[string]any, неупорядоченный документ: короткая запись для быстрых фильтров, где порядок не играет роли. bson.A — слайс значений, массив внутри документа. Моё рабочее разделение: модели — структуры с bson-тегами; ad-hoc запросы и агрегации — bson.D, потому что конвейеры чувствительны к порядку этапов; разовые проверки в тестах — bson.M. Спорное мнение с условием: я не люблю bson.M в продовом коде — опечатка в ключе компилируется и всплывает только на рантайме, — но в тестах и одноразовых скриптах его компактность перевешивает, и там он у меня законен. Если сомневаетесь, какой тип выбрать, — вспомните, важен ли порядок: важен — D, не важен — M, а модели всегда структуры.

Пара типов, которые BSON-кодек откажется кодировать — лучше знать заранее, чем ловить в проде. Каналы, функции и комплексные числа не имеют BSON-представления: кодирование вернёт ошибку вида no encoder found for type, и вставка не состоится. Карты в документе должны быть со строковыми ключами: map[string]any кодируется, а map[int]string — ошибка, потому что ключ документа в BSON всегда строка. Время кодируется типом time.Time: число-миллисекунды вручную лучше не складывать в поле int64, если потом хочется читать его датой. Отсюда рабочее правило: поле модели — либо BSON-нативный тип, либо ваш тип с явной конверсией перед записью; магии «драйвер сам догадается» не существует, и это хорошо — явный код отлаживается быстрее.

D или M: один фильтр двумя способами

GO
// bson.M — map[string]any: коротко, порядок ключей не задан
fM := bson.M{"status": "paid", "total": bson.M{"$gt": 1000}}

// bson.D — срез пар: порядок операторов сохранён как написано
fD := bson.D{
        {Key: "status", Value: "paid"},
        {Key: "total", Value: bson.D{{Key: "$gt", Value: 1000}}},
}

// оба кодируются в одинаковый документ — выбор за читаемостью и порядком

Один и тот же фильтр двумя типами из пакета bson. bson.M компактнее и читается одним глазом; bson.D многословнее, но гарантирует порядок полей — он обязателен там, где последовательность значима: команды сервера и конвейеры агрегаций. Для моделей остаются структуры с тегами, а полный разбор фильтров с $eq, $gt и $in — восьмой урок.

Проверка: дефолтные имена

Как ляжет в BSON поле HTTPCode структуры без bson-тега?

Проверка: omitempty на нуле

Поле Price int64 с тегом `bson:"price,omitempty"` равно 0. Что попадёт в BSON?

Что напечатает MarshalExtJSON?

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

type Item struct {
        Name  string   `bson:"name"`
        Price int64    `bson:"price,omitempty"`
        Tags  []string `bson:"tags,omitempty"`
}

item := Item{Name: "стол"}

data, _ := bson.MarshalExtJSON(item, false, false)
fmt.Println(string(data))

Какой порядок у bson.D?

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

doc := bson.D{
        {Key: "z", Value: 1},
        {Key: "a", Value: 2},
}

data, _ := bson.MarshalExtJSON(doc, false, false)
fmt.Println(string(data))

minsize и inline: редкие, но полезные#

Два модификатора встречаются реже, но знать их стоит. minsize работает с целыми: int64 в Go кодируется в BSON как int64 — восемь байт, даже если число помещается в int32. Тег bson:"qty,minsize" просит кодек писать значение как int32, когда оно влезает: экономия на размере документа, плюс совместимость, когда другой инструмент ожидает int32. Цена минимальна, и на больших коллекциях счётчик байт заметен. inline — совсем другой инструмент: он вливает поля вложенной структуры на верхний уровень документа, как мы видели в разборе — Specs.Color стал color. Работает со структурами и картами, требует, чтобы имена не конфликтовали с полями родителя. Зачем это нужно: композиция моделей без лишнего уровня вложенности — общий блок аудита, инлайнится во многие документы; при декодировании поля соберутся обратно в ту же структуру. Полезно и для миграций: документ уже плоский, а вы хотите работать с ним как с вложенной структурой — inline сделает это без переписывания данных. Пара ограничений: inline-структура должна быть экспортируемым типом, а имена её полей — не конфликтовать с полями родителя, иначе кодек вернёт ошибку ещё на этапе маршалинга, а не молча испортит документ.

Допишите bson-теги

Восстановите теги: имя username для поля Name, omitempty у возраста, inline у метаданных и исключение внутреннего поля.

type User struct { ID bson.ObjectID `bson:"_id,omitempty"` Name string `bson:""` Age int `bson:"age,"` Meta Metadata `bson:","` Internal string `bson:""` }

Соберите структуру и кодирование

Расставьте строки в порядке программы: объявление типа, поля, закрывающая скобка, кодирование, проверка ошибки, вывод.

Строки

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

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

Проверка: тег со минусом

Поле с тегом `bson:"-"` — что с ним произойдёт?

Декодирование: обратная дорога#

Теги работают в обе стороны, и это стоит проговорить до того, как появится первый FindOne. Декодер берёт BSON-документ и заполняет структуру: имя поля в документе сверяется с bson-тегами, отсутствующие поля остаются нулевыми значениями Go, лишние в документе — игнорируются молча. Последние два факта — источник «тихих» багов: если в структуре нет поля amount, а в документе оно есть, драйвер не пожалуется — данные просто не доедут до кода. Обратное тоже верно: если типы несовместимы — строка в документе и int64 в структуре — Decode вернёт ошибку, и её нужно проверять так же честно, как ошибку запроса. Отсюда правило: структура-модель должна описывать документ целиком, а для гибкости есть поля bson.Raw и bson.M — их разберём по ходу курса. Обратный путь, кстати, доступен и без базы: bson.Unmarshal(data, &product) применяет те же теги к байтам — удобно для тестов кодека. А bson.Raw — это документ без декодирования: байты с ленивым доступом к полям через Lookup, когда нужен один ключ из большого документа и распаковывать всё расточительно. Проверять связку «структура — документ» удобно через MarshalExtJSON, как в этом уроке: он чистый Go без сети. Но полноценный круг вставки и чтения требует живого MongoDB — песочница сайта показывает тематическую программу на чистом Go, поэтому код уроков гоняйте локально: контейнер из второго урока и mongosh показывают всю картину.

Карточки: bson-теги

Термин

bson-тег

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

Тег поля структуры: `bson:"имя,модификаторы"`. Без тега драйвер пишет имя поля строчными буквами целиком.

1 / 6

Кодек под контролем: имена через теги, omitempty с пониманием нулей, inline для композиции, минус для исключений, D против M по ситуации. Правило про пару тегов стоит держать с первого дня: если структура уезжает и в HTTP-ответ, и в коллекцию, json и bson теги ставятся рядом — json:"name" bson:"name" — и имена в API и в базе не разъезжаются; классическая болезнь «в ответе одно, в коллекции другое» лечится ревью-правилом: новое поле структуры без пары тегов — замечание. Дальше это знание работает сразу: в уроке про InsertOne и InsertMany структуры с тегами ложатся в коллекцию, в уроке про FindOne декодируются обратно, а в уроке про слой репозитория модели с тегами отделяются от HTTP-структур — там пара json-bson станет отдельной темой. Идентификаторы заслуживают отдельного внимания: в уроке про ObjectID вскроем 12 байт и покажем, зачем _id с omitempty — стандартный первый тег в каждой модели. Фундамент на этом закончен: дальше курс пойдёт по CRUD-глаголам, и каждая структура, которую вы напишете с тегами, будет работать и в записи, и в чтении без единой правки.

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

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

Вывод

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

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

  • Без тега кодек пишет имя поля строчными: HTTPCode станет httpcode — имена задавайте явно.
  • Порядок полей структуры — порядок в документе; _id традиционно первое поле с omitempty.
  • omitempty пропускает false, нули, пустые строки, nil и пустые коллекции; легитимные нули — через указатели.
  • minsize экономит байты на int64, inline вливает вложенную структуру, "-" исключает поле.
  • bson.D — упорядоченный, bson.M — map: структуры для моделей, D для команд и конвейеров, M для набросков.
  • Декодирование: отсутствующие поля — нули Go, лишние — игнор; структура-модель описывает документ целиком.
  • MarshalExtJSON показывает итоговый документ без базы — быстрый способ проверить теги.

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

Возьмите любую модель из своих проектов и перепишите её на bson-теги: имя в snake_case, _id с omitempty, omitempty только там, где ноль — не данные (легитимные нули переведите на указатели). Проверьте себя через bson.MarshalExtJSON без базы, а потом вставьте документ в локальный MongoDB из второго урока и сравните глазами mongosh с выводом MarshalExtJSON — они обязаны совпасть.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · Структуры и bson-теги)

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

Нужны ли bson-теги, если имя поля и так подходит?

Дефолт приводит имя к нижнему регистру целиком: Name станет name — совпадение с желаемым случается, но случайно. Явные теги делают контракт читаемым и спасают от сюрпризов с CamelCase-именами вроде HTTPCode.

Можно обойтись тегами json без bson?

Можно — кодек увидит нижнерегистровые имена полей, и для простых случаев даже заработает. Но json и bson расходятся в деталях: имя в API и имя в документе развяжутся незаметно. Пара json+bson — два десятка символов, которые предотвращают целый класс багов.

Как увидеть документ, который драйвер отправит на сервер?

bson.MarshalExtJSON(struct, false, false) вернёт расширенный JSON ровно того кодека, которым драйвер кодирует BSON: имена, omitempty и inline видны без базы. Для обратного пути есть bson.Unmarshal.

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

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

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

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

Zerolog в Go: быстрое JSON-логирование

Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.

  • Писать структурированные JSON-логи с нуля аллокаций
  • Выбирать уровни и поля события осознанно
  • Настраивать ConsoleWriter для dev и JSON для прода
  • Строить сублоггеры и контекстные поля на запрос