> Что такое пагинация? (Go)

Уровень: junior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: InDrive

Стек: Go

> Пример ответа

Короткий ответ

Пагинация - это механизм разбиения большого набора данных на последовательные страницы фиксированного размера. В backend-разработке она используется для ограничения объёма данных, передаваемых клиенту за один запрос, что снижает нагрузку на БД, сеть и память. Основные параметры - page (номер страницы) и page_size (количество элементов), либо offset и limit. В Go пагинация реализуется через SQL-запросы с LIMIT и OFFSET или через cursor-based подход для больших объёмов.

Подробное объяснение

Пагинация решает проблему передачи больших выборок данных. Без неё клиент получил бы все записи, что приводит к медленным ответам, высокому потреблению памяти и нестабильной работе API.

Существует два основных подхода:

  1. Offset-based (page-based) - использует LIMIT и OFFSET в SQL. Клиент передаёт номер страницы и размер. Пример: LIMIT 20 OFFSET 40 вернёт записи с 41-й по 60-ю. Прост в реализации, но имеет проблемы при больших OFFSET - БД всё равно сканирует все пропущенные строки, что замедляет запрос.

  2. Cursor-based (keyset) - использует уникальное поле (например, id или created_at) как маркер позиции. Клиент передаёт значение курсора из последней записи предыдущей страницы. Пример: WHERE id > 12345 ORDER BY id LIMIT 20. Эффективен для больших таблиц, стабилен при изменении данных, но сложнее в реализации и не позволяет перейти на произвольную страницу.

В REST API пагинация обычно оформляется через query-параметры: ?page=2&page_size=20 или ?limit=20&offset=40. Ответ часто содержит метаданные: total, page, page_size, has_next.

В Go пагинация реализуется на уровне репозитория - в SQL-запросах, и на уровне хендлера - парсинг параметров и валидация. Для cursor-based часто используются time.Time или int64 id.

На практике

В Go-проекте пагинация обычно выглядит так:

  • Хендлер принимает query-параметры, валидирует их (page >= 1, page_size в разумных пределах, например, 1-100).
  • Репозиторий принимает структуру с параметрами пагинации и формирует SQL-запрос.
  • Ответ содержит данные текущей страницы и метаданные для построения навигации.

Для offset-based пагинации важно также получить общее количество записей - отдельным COUNT(*) запросом, чтобы клиент мог понять, сколько всего страниц.

Для cursor-based пагинации в ответе возвращается next_cursor - значение поля, по которому идёт сортировка, из последней записи. Клиент передаёт его в следующем запросе.

В Go часто используют структуру вида:

GO
type Pagination struct {
Page int
PageSize int
}

Или для cursor-based:

GO
type CursorPagination struct {
Cursor int64
Limit int
}

Пример кода

Пример offset-based пагинации в Go с использованием database/sql:

GO
type PaginationParams struct {
Page int
PageSize int
}
type PaginatedResponse struct {
Data []Item `json:"data"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
HasNext bool `json:"has_next"`
}
func GetItems(db *sql.DB, params PaginationParams) (PaginatedResponse, error) {
offset := (params.Page - 1) * params.PageSize
var total int
err := db.QueryRow("SELECT COUNT(*) FROM items").Scan(&total)
if err != nil {
return PaginatedResponse{}, err
}
rows, err := db.Query(
"SELECT id, name FROM items ORDER BY id LIMIT $1 OFFSET $2",
params.PageSize, offset,
)
if err != nil {
return PaginatedResponse{}, err
}
defer rows.Close()
items := []Item{}
for rows.Next() {
var item Item
if err := rows.Scan(&item.ID, &item.Name); err != nil {
return PaginatedResponse{}, err
}
items = append(items, item)
}
hasNext := offset+len(items) < total
return PaginatedResponse{
Data: items,
Total: total,
Page: params.Page,
PageSize: params.PageSize,
HasNext: hasNext,
}, nil
}

Пример cursor-based пагинации:

GO
func GetItemsByCursor(db *sql.DB, cursor int64, limit int) ([]Item, int64, error) {
rows, err := db.Query(
"SELECT id, name FROM items WHERE id > $1 ORDER BY id LIMIT $2",
cursor, limit,
)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := []Item{}
var lastID int64
for rows.Next() {
var item Item
if err := rows.Scan(&item.ID, &item.Name); err != nil {
return nil, 0, err
}
items = append(items, item)
lastID = item.ID
}
return items, lastID, nil
}

Как отвечать на собеседовании

Начни с определения: пагинация - это разбиение данных на страницы. Затем упомяни два подхода - offset-based и cursor-based, объясни разницу и когда что использовать. Для junior-уровня достаточно уверенно объяснить offset-based, но знание cursor-based будет плюсом.

Расскажи, как это реализуется в Go: парсинг параметров, валидация, SQL-запросы с LIMIT/OFFSET, формирование ответа с метаданными. Если спросят про производительность - отметь, что большие OFFSET медленные, и cursor-based решает эту проблему.

Приведи пример кода, если попросят. Не углубляйся в сложные темы вроде индексов или шардирования, если не спрашивают.

Что проверяет интервьюер

Интервьюер проверяет:

  • Понимание базовой проблемы - зачем нужна пагинация.
  • Знание SQL-конструкций LIMIT и OFFSET.
  • Умение проектировать API-параметры и ответ.
  • Понимание trade-off между offset-based и cursor-based.
  • Способность написать корректный код на Go с обработкой ошибок и валидацией.

Для junior важно показать, что ты понимаешь, как пагинация влияет на производительность и UX, и умеешь реализовать простой вариант без ошибок.

Типичные ошибки

  • Не валидировать параметры: page = 0 или page_size = 100000 - нужно ограничивать.
  • Забывать про общее количество записей - клиент не сможет построить навигацию.
  • Использовать OFFSET для больших таблиц без понимания проблем производительности.
  • Не обрабатывать ошибки SQL-запросов.
  • Путать page и offset: page = 2, page_size = 20 - это offset = 20, а не 40.
  • Возвращать next_cursor как int64 без учёта, что поле может быть не числовым.
  • Не закрывать rows через defer - утечка соединений в Go.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью