Содержание урока · 10 разделов
- Фильтры mongodb в Go: документ как условие
- Операторы сравнения одним списком в таблицу
- bson.M фильтры: неявный AND и вложенность
- $or, $and и порядок: когда нужен bson.D
- Правила фильтров одним списком
- $exists и типы: тонкие места условий
- Один словарь на все глаголы: фильтры в Update и Delete
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
До сих пор фильтры выглядели игрушечно: bson.M{"sku": "kb-87-tkl"} — найди по ключу. Настоящие страницы каталога спрашивают иначе: товары в наличии дешевле пяти тысяч, заказы в статусах «оплачен» и «отправлен», пользователи, у которых вообще не заполнен телефон. Всё это — условия, и MongoDB выражает их тем же материалом, что и данные: фильтр — это BSON-документ, в котором ключи называют поля, а значения задают требования. Никакого отдельного языка запросов в Go нет: вы собираете документ, драйвер кодирует его в BSON, сервер сравнивает с каждым документом коллекции. Отсюда и сила подхода, и его ловушки. Сила: один и тот же словарь условий принимают Find, FindOne, UpdateOne, UpdateMany, DeleteOne, DeleteMany — выучив фильтры один раз, вы владеете половиной CRUD. Ловушка: документ — это данные, и компилятор Go почти ничего в нём не проверяет; опечатка в операторе или потерянное условие не сломают сборку, а просто изменят смысл запроса. Поэтому в этом уроке мы пройдём от простых сравнений до составных условий и разберём места, где фильтр тихо означает не то, что вы имели в виду. Проверяйте примеры на живом контейнере: песочница сайта показывает тематическую программу на чистом Go, а mongosh с db.collection.find() покажет те же фильтры глазами сервера.
Фильтры mongodb в Go: документ как условие#
Начнём с правила, которое снимает половину вопросов: пара "поле": значение в фильтре — это сравнение на равенство. bson.M{"status": "paid"} и развёрнутый bson.M{"status": bson.M{"$eq": "paid"}} — один и тот же запрос: $eq подразумевается и пишется явно только для симметрии или внутри составных условий. mongo фильтры go-драйвера принимают в любом виде документа: bson.M для набросков, bson.D там, где важен порядок, структуру с тегами, если условие собрано заранее. Сервер читает документ сверху: каждое поле фильтра — отдельное требование, и документ совпадает, только когда выполнены все требования сразу. Это неявный AND, и он покрывает большинство реальных запросов: статус и диапазон цены и наличие — три ключа в одном bson.M. Операторы сравнения вкладываются в значение поля: bson.M{"price": bson.M{"$gt": 1000}} читается как «price больше 1000». Вложенный документ может нести несколько операторов сразу — "$gte" и "$lte" рядом дадут диапазон с включёнными границами. И важная деталь про типы: сравнение в MongoDB строгое по классам BSON-типов — число 42 и строка "42" не равны никогда, зато все числовые типы сравниваются между собой как числа: int32, int64 и double в равенстве совместимы, и в сортировке сервер расставит их по значению, а не по разновидности. Если фильтр «не находит» документ, который глазами есть в коллекции, первым делом проверяйте тип значения в mongosh.
Три рабочих фильтра: равенство, диапазон, список
GOcoll := client.Database("shop").Collection("products")
// равенство: $eq подразумевается
f1 := bson.M{"in_stock": true}
// диапазон: два оператора в одном вложенном документе
f2 := bson.M{"price": bson.M{"$gte": 1000, "$lte": 5000}}
// список значений: $in со срезом
f3 := bson.M{
"status": bson.M{"$in": []string{"paid", "shipped"}},
"total": bson.M{"$gte": 1000},
}
for _, f := range []bson.M{f1, f2, f3} {
n, err := coll.CountDocuments(ctx, f)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v -> %d документов
", f, n)
}Три уровня сложности в одном списке. f1 — неявный $eq: пара поле-значение. f2 — диапазон: операторы $gte и $lte живут внутри вложенного документа у одного поля, границы включены. f3 — два условия сразу: $in принимает срез допустимых значений, второе поле добавляет неявный AND. CountDocuments здесь — дешёвый способ проверить, сколько документов отвечает фильтру, не тянув их в память.
Операторы сравнения одним списком в таблицу#
| Оператор | Смысл | Пример |
|---|---|---|
| $eq | равенство (подразумевается без записи) | bson.M{"status": "paid"} |
| $ne | не равно | bson.M{"status": bson.M{"$ne": "draft"}} |
| $gt / $gte | больше / больше или равно | bson.M{"price": bson.M{"$gte": 1000}} |
| $lt / $lte | меньше / меньше или равно | bson.M{"price": bson.M{"$lte": 5000}} |
| $in | значение из списка | bson.M{"status": bson.M{"$in": []string{"paid"}}} |
| $nin | значения нет в списке | bson.M{"status": bson.M{"$nin": []string{"draft"}}} |
| $exists | поле существует (или нет при false) | bson.M{"deleted_at": bson.M{"$exists": false}} |
bson.M фильтры: неявный AND и вложенность#
Почему bson.M стал дефолтом для условий: фильтры в девяти случаях из десяти — unordered-документ, где порядок полей не влияет на результат, а компактность map-литерала экономит глаза. bson.m фильтры собираются по двум правилам. Первое: несколько ключей верхнего уровня — это AND; «статус оплачен и сумма от тысячи» пишется двумя ключами, без всяких $and. Второе: все операторы поля живут в одном вложенном документе-значении — и диапазон, и список, и существование. Из этих правил вытекает самое популярное место ошибки у новичков: попытка написать два условия для одного поля двумя ключами. В литерале Go такой код не скомпилируется — duplicate key in map literal, и компилятор прав. Но если фильтр собирается динамически, через добавление в карту, второй m["price"] молча перезапишет первый, и запрос станет короче на одно условие; об этом — отдельный коллаут ниже. Ещё одна тонкость bson.M: ключи — строки, и опечатка в имени поля не поймается нигде до момента, когда запрос вернёт пустоту; поэтому фильтры для постоянных запросов объявляют через переменные с именами полей — var fieldPrice = "price" — или через структуры с тегами. В пятом уроке мы уже договаривались: bson.M законен в тестах и разовых проверках, в продовом коде имя поля обязано прийти из константы.
Кейс: фильтр отчёта собирается динамически — m["price"] = range1, потом ветка кода добавляет m["price"] = range2. Перезапись молча: карта не умеет хранить два значения под одним ключом, ошибок нет, запрос просто ищет по последнему условию. Что ломается: отчёт разъезжается с ожиданиями — вместо «цена в обоих диапазонах» ищется «цена во втором»; баг живёт до первого сверки с базой. Лекарство: условия одного поля объединяйте в одном вложенном документе "$and" или в одном значении-операторе, а сборку фильтра накрывайте тестом на количество совпадений.
$or, $and и порядок: когда нужен bson.D#
Неявный AND не умеет «или» — за этим приходит логический оператор $or: он принимает массив условий, из которых достаточно любого. bson.M{"$or": []bson.M{{"status": "paid"}, {"total": bson.M{"$gte": 10000}}}} найдёт и оплаченные, и просто дорогие заказы. Внутри каждого элемента массива действует тот же неявный AND, поэтому ветки получаются сколь угодно сложными. Когда же $and пишут явно? В двух случаях: несколько условий на одно и то же поле — "$and": [{"price": {"$gt": 100}}, {"price": {"$lt": 500}}] — и точный контроль над порядком условий, который иногда влияет на план запроса. И вот здесь появляется bson.D: логические операторы — это операторы сервера, а массив условий — упорядоченная структура, и драйвер ждёт, что порядок сохранён. Мера жёсткости у v2 показательная: если передать unordered-карту туда, где порядок значим — например, в сортировку через options.Find().SetSort(), — драйвер отклонит аргумент ошибкой ErrMapForOrderedArgument ещё на клиенте. Фильтры верхнего уровня остаются территорией bson.M, но всё, что внутри $or и $and, привычнее писать через bson.D: визуально совпадает с документацией сервера и не вызывает вопросов при чтении. Полный разбор $regex для поиска по подстроке и комбинаций логических операторов — четырнадцатый урок.
Составное условие: $or поверх неявного AND
GO// статус в двух «живых» значениях И (сумма от 10 тысяч ИЛИ скидка есть)
filter := bson.M{
"status": bson.M{"$in": []string{"paid", "shipped"}},
"$or": []bson.M{
{"total": bson.M{"$gte": 10000}},
{"discount": bson.M{"$gt": 0}},
},
}
cursor, err := coll.Find(ctx, filter)
if err != nil {
return fmt.Errorf("find orders: %w", err)
}
defer cursor.Close(ctx)
var orders []Order
if err := cursor.All(ctx, &orders); err != nil {
return fmt.Errorf("collect orders: %w", err)
}Фильтр читается как фраза: статус из списка — неявный AND с блокой $or; внутри $or каждая ветка — самостоятельное условие, и достаточно любой. Массив для $or собран из bson.M-литералов — порядок элементов массива драйвер сохранит, а карта верхнего уровня не мешает. Дальше — знакомый каркас из седьмого урока: Find, проверка ошибки, defer Close, All в срез.
Правила фильтров одним списком#
- Пара "поле": значение — неявный $eq; разворачивать без нужды не стоит.
- Несколько ключей в одном документе — AND; отдельных операторов для него не нужно.
- Операторы поля живут в одном вложенном значении: $gte и $lte рядом — диапазон.
- Условия одного поля не пишутся двумя ключами: map перезапишет, а не сложит.
- $or и $and принимают массивы условий — внутри ветки работает тот же AND.
- Имена полей — из констант: опечатка в строке не поймается компилятором.
- Сравнение строгое по типам: 42 и "42" — разные значения; сверяйтесь с mongosh.
$exists и типы: тонкие места условий#
Два условия, вокруг которых накопилось больше всего недоумений. Первое — $exists: он проверяет наличие поля в документе, и это не то же самое, что проверка на nil. Запрос bson.M{"phone": bson.M{"$exists": false}} найдёт документы, у которых поля phone нет вообще; а фильтр bson.M{"phone": nil} матчит и отсутствие поля, и поле со значением null — null в MongoDB означает «поле есть, значения нет», и равенство null специально расширено на пропущенные поля. Для мягкого удаления разница принципиальна: deleted_at отсутствует у живых записей и равна null — у «обнулённых» вручную; выбирайте семантику осознанно, мы вернёмся к ней в уроке про удаление. Второе тонкое место — типы в сравнениях: $gt по числовому полю сравнивает числа с числами, но документ, в котором цена по случайности записана строкой "1000", в диапазон не попадёт и ошибки не вызовет; $type в таких запросах вычисляет предателей. Это плата за схему без валидации — и аргумент за валидатор коллекции, до которого дойдём в двадцать втором уроке. Практика для отладки: сомневаетесь, почему документ не матчился, — воспроизведите фильтр в mongosh и сравните типы через typeof; расхождение BSON-типов всплывает быстрее, чем любую логику перечитывать.
Фильтр без требований к порядку — bson.M: короче и читается легче. Внутри $or и $and, в сортировках и командах — bson.D: порядок сохранён, а драйвер не отклонит аргумент. Структуры с тегами — для фиксированных фильтров-констант. Выбор из пятого урока никуда не делся: важен порядок — D, не важен — M, модели — структуры.
Один словарь на все глаголы: фильтры в Update и Delete#
Недооценённая выгода единого языка условий: фильтры не обслуживают только чтение. UpdateOne, UpdateMany, DeleteOne и DeleteMany принимают ровно тот же документ-условие, что и Find: неявный AND, операторы сравнения, $in и $or — всё работает в операциях записи без единого изменения. Практически это значит, что трудозатраты окупаются дважды: собрал и отладил фильтр «просроченные сессии» для выборки — тот же документ завтра пойдёт в UpdateMany, чтобы пометить их истёкшими, и в DeleteMany, чтобы почистить. Есть и нюансы. В операциях записи фильтр работает по тем же правилам типов и null-семантики, поэтому осторожность с {"поле": nil} важна вдвойне: под такой фильтр попадёт и отсутствие поля, и обновление затронет неожиданный документ. Составные условия в записи проверяются тем же CountDocuments до вызова: сначала посчитали, сколько документов ответит фильтру, потом отправили обновление — дешёвая страховка от пакетной операции по не тому множеству. И последнее: записи — территория уникальных индексов и валидаторов, о которых поговорим в семнадцатом и двадцать втором уроках; фильтр может быть сколь угодно хитрым, но быстрым его делает индекс, а безопасным — схема.
Проверка: неявный AND
Фильтр bson.M{"status": "paid", "total": bson.M{"$gte": 1000}} выбирает документы, у которых...
Проверка: диапазон цены
Как найти документы с ценой от 100 до 500 включительно?
Какие документы найдёт фильтр?
Прочитайте код и выберите, что он выведет:
// коллекция orders:
// {sku: "a", status: "paid", total: 1500}
// {sku: "b", status: "returned", total: 3000}
// {sku: "c", status: "paid", total: 800}
// {sku: "d", status: "shipped", total: 2000}
f := bson.M{
"status": bson.M{"$in": []string{"paid", "shipped"}},
"total": bson.M{"$gte": 1000},
}
n, _ := coll.CountDocuments(ctx, f)
fmt.Println(n)Соберите составной фильтр
Восстановите операторы: нижняя граница, верхняя граница и список допустимых статусов.
Соберите запрос с составным фильтром
Расставьте строки по порядку: фильтр, запрос, страховка закрытия, сбор результата.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: операторы фильтров
Словарь условий собран: неявный AND для повседневных запросов, операторы сравнения в одном вложенном значении, $or для альтернатив, $exists для различения «нет поля» и «пусто». Дальше этот же словарь заработает в записи: в уроке про UpdateOne и UpdateMany фильтр выберет документы, которые поменяют операторы $set и $inc. Сложные комбинации — $regex, вложенные $and, поиск по массивам — тема урока про $and, $or и $regex. А пока фильтры гоняются по всей коллекции, скорость держится на честном слове: в уроке про индексы покажем, как превратить частые фильтры из сканирования в поиск по дереву. Практическое задание на закрепление: возьмите три реальных запроса из своего проекта и перепишите их с $in и диапазонами; каждый проверьте в mongosh тем же документом — совпадение количества документов в консоли и в CountDocuments лучший тест на то, что Go-фильтр означает ровно то, что вы имели в виду.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Фильтр — BSON-документ: ключи называют поля, значения задают требования; один словарь работает в Find, Update и Delete.
- Пара "поле": значение — неявный $eq; несколько ключей — неявный AND.
- Операторы одного поля живут в одном вложенном документе: {"$gte": 100, "$lte": 500} — диапазон с включёнными границами.
- $in принимает срез значений; $or — массив условий, из которых достаточно любого.
- Два условия на одно поле — через один оператор-документ или явный $and: карта перезапишет ключ молча.
- $exists:false — поля нет; {"поле": nil} матчит и null, и отсутствие: семантики разные.
- Сравнения строгие по BSON-типам: 42 и "42" не равны; для порядка значимых мест — bson.D.
Домашнее задание
Возьмите коллекцию заказов из прошлых уроков и напишите три фильтра: диапазон суммы с включёнными границами, список из трёх статусов через $in и комбинацию «оплачен ИЛИ сумма больше N» через $or. Каждый фильтр прогоните и через CountDocuments в Go, и через db.orders.find() в mongosh — количества обязаны совпасть. Отдельно попробуйте собрать фильтр в цикле с перезаписью ключа и посмотрите, как запрос молча худеет.
Личные заметкиПусто
Вопросы и ответы по уроку
bson.M или bson.D для фильтров — что выбрать по умолчанию?
По умолчанию bson.M: фильтры не чувствительны к порядку полей, а map-литерал читается легче. bson.D нужен внутри $or и $and с массивами условий, в сортировках и там, где порядок операторов значим; драйвер сам отклонит карту в порядке-зависимых местах ошибкой ErrMapForOrderedArgument.
Почему фильтр не находит документ, который есть в коллекции?
Три частые причины: опечатка в имени поля, несовпадение BSON-типа (строка вместо числа) и молчаливая перезапись ключа при динамической сборке. Проверьте фильтр тем же документом в mongosh и сравните типы значений через typeof — расхождение видно сразу.
Чем отличается {"поле": nil} от {"поле": {"$exists": false}}?
Первое матчит документы, где поле равно null, и где поля нет вообще — равенство null в MongoDB намеренно расширено. Второе требует именно отсутствия поля. Для мягкого удаления это разные множества: «обнулённая» запись и «никогда не имевшая» — не одно и то же.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Zerolog в Go: быстрое JSON-логирование
Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.
- Писать структурированные JSON-логи с нуля аллокаций
- Выбирать уровни и поля события осознанно
- Настраивать ConsoleWriter для dev и JSON для прода
- Строить сублоггеры и контекстные поля на запрос