Как устроена пагинация в Go проектах
Что проверяют
Знаете ли вы, чем offset-пагинация плоха на больших таблицах, и умеете ли сделать курсорную. Следом спрашивают, что будет, если между запросами двух страниц в таблицу вставили запись, и как отдавать курсор клиенту.
Как отвечать
В Go-сервисе пагинация делается в SQL, а не срезом слайса в памяти. Подходов два:
- LIMIT/OFFSET:
ORDER BY id LIMIT $1 OFFSET $2. Просто и позволяет прыгнуть на страницу 50. Но база всё равно читает и выбрасывает все пропущенные строки, поэтому глубокие страницы тормозят, а при вставках записи съезжают: одна повторяется, другая пропадает; - keyset (курсор):
WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3. Запрос идёт по индексу(created_at, id)и стоит одинаково на любой глубине. Цена: на произвольную страницу не перейти, только «дальше» и «назад»; - курсор для клиента: значения последней строки упаковывают в непрозрачную строку, например JSON в
base64.RawURLEncoding, и отдают какnext_cursor. Клиент курсор не собирает, только возвращает.
Сортировка обязана быть уникальной: одного created_at мало, id нужен как тайбрейкер. Чтобы понять, есть ли следующая страница, без отдельного COUNT(*) запрашивайте limit+1 строк и лишнюю отрезайте.
В хендлере параметры читаются из r.URL.Query(), парсятся через strconv.Atoi с проверкой ошибки, а limit ограничивается сверху, скажем сотней, иначе клиент попросит миллион строк. В gRPC-сервисах то же принято оформлять полями page_size и page_token. Точный COUNT(*) по большой таблице дорогой, его часто заменяют оценкой или убирают из ответа.
Где спотыкаются
Игнорируют ошибку strconv.Atoi. Пустой page превращается в 0, (page-1)*limit уходит в минус, и выражение items[start:end] паникует.
Строят курсор по неуникальному полю. Записи с одинаковым временем создания теряются на границе страниц.
Считают, что индекс спасает OFFSET. Он помогает быстро найти начало, но пропускаемые строки всё равно перебираются, так что OFFSET 100000 медленный и с индексом.