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

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

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

Урок 12 из 25 ~16 мин +50 XP

ObjectID: анатомия идентификатора

ObjectID — это идентификатор, который mongo-драйвер ставит в _id автоматически, и внутри его 24 hex-символов спрятаны время создания, отпечаток процесса и счётчик. Разберём анатомию из 12 байт, тип bson.ObjectID в Go, сортировку по времени создания — и случаи, когда на неё нельзя полагаться.

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

В логах и URL он встречается каждый день: 68a1f3c4b2e9d7056124a8f3 — строка из 24 hex-символов, которую поддержка просит прислать, а дизайнер API кладёт в путь /products/{id}. В четвёртом уроке мы оставили InsertOne без _id, и сервер сгенерировал его сам — вернув в InsertedID. Теперь вскроем это значение: ObjectID не случайный хэш, а структурированная конструкция из 12 байт, в которой зашито время создания документа. Из этого следуют практические суперсилы: дату создания можно узнать без обращения к базе, список документов по _id отсортировать «от старых к новым» почти бесплатно, а пагинацию построить на диапазоне идентификаторов. Из этого же следуют и ловушки: точность всего секунда, порядок внутри секунды не совпадает с порядком вставки, а часы разных машин врут относительно друг друга. Урок собран вокруг типа bson.ObjectID из второго мажора драйвера — в v1 он жил в пакете primitive, и половина старых туториалов отсюда не компилируется.

Анатомия ObjectID: 12 байт вместо 24 символов#

Хранится ObjectID как 12 сырых байт, а 24 hex-символа — это лишь текстовое представление: каждый байт кодируется двумя символами. Внутри — три секции. Первые четыре байта — время в секундах от начала эпохи Unix, записанное big-endian: это и есть время создания, с точностью до секунды. Следующие пять байт — уникальный отпечаток процесса: случайное значение, которое драйвер выбирает один раз при старте процесса и дальше ставит во все идентификаторы этого процесса. Последние три байта — счётчик, который инкрементируется атомарно на каждый новый ObjectID; стартует он тоже со случайного значения, чтобы два процесса не начали с одинаковой точки. Складывая секции, получаем свойства идентификатора: он уникален без единого обращения к серверу, монотонно растёт внутри одного процесса и почти упорядочен по времени между машинами. Именно поэтому mongo objectid стал дефолтным _id в mongo: вставка не требует координации, а база выигрывает плотность записей в индексе — новые документы ложатся в правый край B-дерева, а не раскидываются по всему дереву. Big-endian здесь не украшение: старший байт времени идёт первым, поэтому побайтовое сравнение идентификаторов — это и есть сравнение по времени. Счётчик — три байта, больше шестнадцати миллионов значений: атомарный инкремент на каждый вызов, при переполнении отсчёт начинается заново. У секции времени есть и побочный эффект: hex открывает дату создания любому, кто видит URL, — для номеров заказов это иногда нежелательно, и ниже разберём альтернативы.

Hex, Timestamp и обратная дорога

GO
func inspect(id bson.ObjectID) {
        // время создания зашито в первых четырёх байтах
        fmt.Println("hex:", id.Hex())
        fmt.Println("создан:", id.Timestamp().Format(time.RFC3339))

        fromURL, err := bson.ObjectIDFromHex("68a1f3c4b2e9d7056124a8f3")
        if err != nil {
                log.Fatal(err) // the provided hex string is not a valid ObjectID
        }
        fmt.Println(fromURL) // ObjectID("68a1f3c4b2e9d7056124a8f3")
}

Hex возвращает 24 hex-символа — то, что видно в URL и mongosh; Timestamp достаёт время создания без обращения к базе, в UTC. ObjectIDFromHex — обратная дорога из строкового параметра: строка обязана быть ровно 24 символа, иначе ErrInvalidHex. Print печатает через String() — формат ObjectID("...") из исходников драйвера.

bson.ObjectID в Go: типы и методы#

В v2 драйвера тип живёт в пакете bson: bson.ObjectID — это массив [12]byte со скромным набором методов, и все они целиком клиентские: ни один не обращается к серверу. NewObjectID() генерирует идентификатор от текущего времени — тот же код, что стоит за автогенерацией _id при вставке. NewObjectIDFromTimestamp(t) собирает ObjectID от произвольной метки — удобно для «синтетических границ» при запросах по диапазону времени, об этом ниже. IsZero() проверяет нулевое значение: пустой ObjectID — это NilObjectID, и отличить «не задан» от «получен из базы» помогает именно он. ObjectIDFromHex и Hex — пара для текстового представления: параметр запроса входит строкой, а уезжает в фильтр уже байтами. Отдельно про сериализацию: ObjectID реализует encoding.TextMarshaler, поэтому в JSON-ответах API автоматически превращается в hex-строку, а не в массив байт — одним пакетом encoding/json решён вопрос «как отдавать _id наружу». Обратное направление продумано так же: UnmarshalText принимает и 24 hex-символа, и 12 сырых байт, а пустую строку декодирует в NilObjectID — необязательный параметр API не ломает разбор. Метка времени в NewObjectIDFromTimestamp задаёт только первые четыре байта: отпечаток процесса и счётчик берутся текущие, поэтому два идентификатора от одной метки различаются. Имена конструкторов переезжать не заставляют: NewObjectID и ObjectIDFromHex называются одинаково в обеих версиях — меняется только пакет. Старый код с primitive.ObjectID переносится заменой импорта: поля, конструкторы и методы совпадают один в один.

Методы bson.ObjectID#

МетодЧто возвращаетЗаметка
Hex()24 hex-символа строкойформат для URL, логов и mongosh
Timestamp()time.Time из первых 4 байтвсегда UTC, точность — секунды
IsZero()true для NilObjectIDпроверка «идентификатор не задан»
ObjectIDFromHex(s)ObjectID и ошибку ErrInvalidHexстрока обязана быть ровно 24 символа
NewObjectID()новый ObjectID от time.Now()тот же код, что за автогенерацией _id
String()ObjectID("hex")удобен в логах и сообщениях об ошибках

Сортировка по _id: что гарантировано#

Поскольку время стоит в первых байтах, сортировка по _id — это почти сортировка по времени создания: SetSort(bson.D{{Key: "_id", Value: 1}}) выстроит документы от старых к новым без отдельного поля created_at и без индекса поверх него — _id проиндексирован с рождения. На этом держится и пагинация по диапазону: «документы после этого курсора» записывается как {_id: {$gt: последний}}, и следующий лист читается без дорогого skip, о котором подробно поговорим в тринадцатом уроке. Границы диапазона удобно строить из времени: NewObjectIDFromTimestamp(полночь) даёт идентификатор, меньше которого ничего из этого дня нет, — условие {_id: {$gte: утро}, $lt: следующий день} читает документы дня прямо из индекса. Слово «почти» здесь ключевое, и на нём стоит задержаться. Точность времени — секунда: документы одной секунды различаются уже счётчиком, который растёт внутри одного процесса. Значит, для документов одного процесса порядок внутри секунды стабилен, а вот между разными процессами и машинами гарантий нет. Для выдач пользователю, лент и отчетов этого хватает; для строгой хронологии — нет, и следующий коллаут объясняет почему.

Фильтр по _id из URL#

Самый частый запрос в любом сервисе — «найди по идентификатору из пути»: /products/68a1f3c4... Параметр приходит строкой, а в фильтр должен уехать bson.ObjectID, поэтому конвертация ставится до запроса: id, err := bson.ObjectIDFromHex(raw). Ошибка конвертации — это 400 от вашего хендлера, а не поход на сервер: строка короче 24 символов, с не-hex символами или пустая отсекается мгновенно. Пропустить проверку и подставить сырую строку в фильтр — значит отправить заведомо пустой запрос: сервер молча вернёт отсутствие совпадений, и вместо честной ошибки «неверный формат» пользователь получит загадочное «не найдено». Дальше всё знакомо: bson.M{"_id": id} — самый дешёвый фильтр в базе, значение лежит в уникальном индексе; FindOne с Decode и ErrNoDocuments работают по правилам шестого урока. Обратный путь — id.Hex() в ответе API или в логе — замыкает круг строка-байты-строка, и никакого второго представления изобретать не нужно. В коллекции при этом лежат именно байты: кодек пишет BSON-тип ObjectID длиной двенадцать, а не строку, — вторая копия hex в документе не заводится, и индекс остаётся компактным. Ошибка ErrNoDocuments здесь — нормальный путь «не найдено»: отдавать её наружу 404-й страницей — решение хендлера, а не драйвера, драйвер честно сообщает о пустоте.

Хендлер «найди по идентификатору»

GO
func GetProduct(ctx context.Context, coll *mongo.Collection, hexID string) (Product, error) {
        id, err := bson.ObjectIDFromHex(hexID)
        if err != nil {
                return Product{}, ErrBadID // строка из URL не похожа на ObjectID
        }

        var p Product
        err = coll.FindOne(ctx, bson.M{"_id": id}).Decode(&p)
        if errors.Is(err, mongo.ErrNoDocuments) {
                return Product{}, ErrNotFound
        }
        return p, err
}

Типовой хендлер: строку из URL конвертируем в bson.ObjectID до запроса — невалидная строка отсекается с 400, а не уезжает на сервер. Фильтр по _id — самый дешёвый поиск: значение лежит в уникальном индексе. Дальше всё как в шестом уроке: Decode в структуру и ErrNoDocuments на пустоте через errors.Is.

Что стоит запомнить про ObjectID#

  • 12 байт: 4 — секунды времени создания, 5 — отпечаток процесса, 3 — счётчик.
  • Hex-строка из 24 символов — только представление; хранится и фильтруется байтами.
  • Timestamp() даёт время создания без запроса к базе — всегда UTC, с точностью до секунды.
  • Сортировка по _id — эвристика «от старых к новым»; строгая хронология — отдельное поле created_at.
  • Пагинация по диапазону {_id: {$gt: ...}} дешевле skip на глубоких страницах.
  • Строку из URL конвертируйте ObjectIDFromHex до запроса — ошибку формата отдавайте как 400.

Проверка: структура идентификатора

Из каких секций состоят 12 байт ObjectID?

Проверка: Timestamp без базы

Что вернёт id.Timestamp() для ObjectID, полученного из базы?

Что напечатает Hex() у нуля?

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

var id bson.ObjectID // нулевое значение

fmt.Println(len(id.Hex()), id.IsZero())

Когда ObjectID — не лучший выбор#

Дефолтный _id хорош всем, кроме особых случаев, и их полезно распознавать заранее. Естественные ключи — артикул, ISBN, slug товара — сами уникальны, и двойной идентификатор только путает: такие коллекции живут с _id-строкой. Распределённым системам, где идентификаторы генерируют десятки сервисов и время машин разъезжается, чаще подходит UUID: 16 байт без намёка на время, зато без утечки «когда создан» наружу — для номеров заказов это иногда требование безопасности. Последовательные номера из бизнес-логики — «номер заказа 1042» — собираются через FindOneAndUpdate с $inc, как в десятом уроке, и живут рядом с ObjectID, а не вместо него. Наконец, URL-дизайн: hex из 24 символов длиннее человеческого slug, поэтому каталоги часто имеют и внутренний _id, и публичный slug с уникальным индексом. Правило простое: ObjectID — разумный дефолт, пока не появился внешний ключ, требование скрыть время или последовательность для людей; тогда выбор осознанный, а не привычный. Для случая UUID готового типа в пакете bson нет — MongoDB хранит UUID бинарным сабтипом, драйвер кодирует []byte, и это ещё один аргумент не заводить UUID без необходимости. С slug-подходом связана и привычная пара индексов: уникальный на slug для публичных страниц и отдельный на created_at для сортировок, которые не хочется привязывать к идентификатору.

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

Восстановите конвертацию из строки, фильтр и чтение времени создания.

id, err := bson.(hexID) if err != nil { return err } var doc Product err = coll.FindOne(ctx, bson.M{"": id}).Decode(&doc) fmt.Println("создан:", doc.ID.().Format(time.RFC3339))

Соберите функцию разбора идентификатора

Расставьте строки функции по порядку: проверка длины, конвертация, обработка ошибки, возврат.

Строки

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

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

Карточки: ObjectID

Термин

ObjectID

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

12 байт: 4 байта времени создания в секундах, 5 байт отпечатка процесса, 3 байта счётчика. Hex-представление — 24 символа.

1 / 6

Практика без базы тоже доступна: bson.NewObjectID(), Hex(), Timestamp() и ObjectIDFromHex — чистый Go, и структура идентификатора видна сразу в выводе. Дальше идентификатор работает по всему курсу: в уроке про InsertOne он появлялся в InsertedID, фильтр по _id — базовый пример урока про фильтры, а пагинация по диапазону _id встретится с дорогим skip в уроке про проекции, сортировку и лимиты. Для счётчиков-номеров, которые заметил раздел про «не лучший выбор», пригодится upsert с FindOneAndUpdate. Песочница сайта показывает тематическую программу на чистом Go, поэтому эксперименты с байтами идентификатора гоняйте локально вместе с mongosh — вторая команда из второго урока поднимет всё нужное.

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

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

Вывод

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

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

  • ObjectID — 12 байт: 4 секунды времени, 5 отпечаток процесса, 3 счётчик; hex-строка — лишь представление.
  • В v2 тип называется bson.ObjectID; primitive.ObjectID — маркер кода первой версии.
  • Timestamp() достаёт время создания без базы: всегда UTC, точность секунды.
  • Сортировка по _id — эвристика «от старых к новым»: внутри одного процесса стабильна, между машинами — нет.
  • Строгая хронология живёт в отдельном created_at, а не в идентификаторе.
  • Пагинация по {_id: {$gt: last}} дешевле skip на глубоких страницах.
  • Строку из URL конвертируйте ObjectIDFromHex до запроса: ошибка формата — 400, а не молчаливое «не найдено».
  • MarshalJSON и TextMarshaler отдают hex-строку: JSON-ответы API работают без ручных конвертаций.

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

Соберите утилиту разбора идентификаторов: функция из строки в bson.ObjectID с проверкой длины и ошибкой формата, печать времени создания через Timestamp и разница во времени между двумя ObjectID из ваших данных. Затем подключите её к хендлеру «найди по идентификатору» локально и сравните поведение на валидной строке, невалидной и пустой — лог должен различать 400 и 404. Завершите сборкой границ дня через NewObjectIDFromTimestamp: диапазон по _id от полуночи до следующей полуночи должен находить ровно документы нужного дня.

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

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

Можно ли генерировать ObjectID на клиенте, а не на сервере?

Да, и драйвер делает это сам: bson.NewObjectID() в поле _id с omitempty заполняется до отправки, и сервер ничего не генерирует. Это удобно: идентификатор известен сразу после сборки структуры, до ответа базы. В четвёртом уроке мы видели InsertedID для серверной генерации — с клиентской он просто не нужен.

Насколько надёжна сортировка по _id вместо created_at?

Для списков и лент — надёжна: документы почти всегда выходят «от старых к новым». Для аудита, финансов и споров о порядке — ненадёжна: секунда точности, разные часы машин, шаги NTP. Правило: _id — бесплатная эвристика, created_at — обязательство, и их не подменяют друг другом. Полезно держать и обратный тест: если сортировка по _id когда-нибудь расходится с created_at, разбор начинается с часов и синхронизации времени, а не с драйвера.

Почему Hex даёт 24 символа, а не 12?

Hex-кодировка пишет каждый байт двумя символами: 12 байт превращаются в 24 hex-символа. Обратно работает ObjectIDFromHex: он требует ровно 24 символа и возвращает 12 байт. Поэтому длина строки — первая проверка валидности идентификатора из URL.

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

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

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

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

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

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

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