Пагинация 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 и не потерять машинную проверку причины.