Пагинация Go API: почему новые записи ломают OFFSET и нужен курсор
Пользователь читает вторую страницу списка, а между запросами добавилась новая запись. При OFFSET граница сдвигается, и часть элементов повторяется. Для стабильного движения по упорядоченному потоку часто используют курсор последнего ключа.
Как разобрать ситуацию
В простом примере список отсортирован по возрастающему уникальному id. Курсор 20 означает «вернуть элементы с id больше 20». Если сортировка идёт по времени, курсор должен содержать также дополнительный уникальный ключ для разрешения совпадений. Нельзя сделать устойчивую пагинацию, сохранив неоднозначный порядок.
Пример и ожидаемый результат
Первая страница содержала id 10 и 20. Перед второй появился id 15. Курсорный отбор после 20 возвращает 30 и 40, не повторяя 20. Новая запись 15 не появится в этом проходе — это осознанная граница, которую нужно объяснить клиенту.
package main
import "fmt"
func main() {
ids := []int{10, 15, 20, 30, 40}
out := []int{}
for _, id := range ids {
if id > 20 && len(out) < 2 {
out = append(out, id)
}
}
fmt.Println(out)
}
Программа выводит [30 40]. Она проверяет правило отбора, а SQL-версию и индекс нужно проверить отдельно на фактическом хранилище.
Проверка перед применением
- Проверьте вставку перед текущим курсором и удаление уже просмотренного элемента.
- Проверьте одинаковое время у двух записей при составной сортировке.
- Не доверяйте произвольным полям курсора без проверки типов, доступа и параметров фильтра.
Для API полезно возвращать следующий курсор явно и запрещать смешение с другим фильтром запроса. Иначе клиент может продолжить страницу одного списка в совершенно другом наборе данных. В тестах проверяйте не только длину страницы, но и отсутствие повторов вместе с понятным поведением новых записей.
Границы решения
Курсор не даёт неизменяемого снимка при редактировании ключей сортировки. Для полного консистентного экспорта может потребоваться snapshot или иной контракт. Пример работает с локальным срезом Go и не измеряет производительность базы.
Если базовый Go уже знаком и хочется связать HTTP, хранение данных и обработку отказов в одну систему, посмотрите наш платный курс
Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker. Перед записью сравните программу со своей задачей и подготовкой.
Техническая справка: www.postgresql.org: документация, www.sqlite.org: документация.
Следующий шаг: Go backend: HTTP, конкурентность и устойчивость сервиса. По смежным вопросам: HTTP retry в Go: почему один список кодов не делает повтор безопасным, Как оборачивать ошибки в Go и не потерять машинную проверку причины.
