> Как устроена пагинация в Go проектах (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: sferaplatform.ru
Стек: Go
> Пример ответа
Короткий ответ
Пагинация в Go обычно реализуется через параметры запроса limit и offset или cursor-based подход. На уровне БД используется LIMIT/OFFSET в SQL или Find с Limit/Offset в GORM. Для API принято возвращать метаданные: total, page, per_page. В высоконагруженных системах предпочтителен cursor-based вариант, так как OFFSET деградирует на больших объёмах данных. Валидация параметров и дефолтные значения - обязательная часть реализации.
Подробное объяснение
Существует два основных подхода к пагинации в Go-проектах:
Offset-based пагинация - классический вариант с page и per_page или limit и offset. Прост в реализации, позволяет переходить к произвольной странице. Минус - при больших значениях OFFSET база данных сканирует все пропущенные строки, что замедляет запрос. Подходит для небольших и средних объёмов данных.
Cursor-based пагинация - использует непрозрачный токен (обычно закодированный ID или timestamp последней записи). Запрос строится через WHERE id > $cursor ORDER BY id LIMIT $limit. Работает стабильно на любых объёмах, но не даёт перейти к произвольной странице. Часто применяется в realtime-системах, лентах, чатах.
В Go-проектах пагинация обычно реализуется в слое handler/repository. Типичная структура:
- DTO с параметрами запроса (
Limit,Offset,Cursor) - Валидация и установка дефолтов
- Формирование запроса к БД
- Ответ с данными и метаинформацией
Для GORM стандартный подход:
GOdb.Model(&User{}).Limit(limit).Offset(offset).Find(&users)
Для SQL-запросов через database/sql - подстановка параметров в LIMIT $1 OFFSET $2.
Важно учитывать: LIMIT и OFFSET принимают целые числа, поэтому параметры из query string нужно парсить через strconv.Atoi и проверять на отрицательные значения.
На практике
В реальных проектах пагинация редко ограничивается простым LIMIT/OFFSET. Часто добавляют:
- сортировку (параметр
sortс валидацией допустимых полей) - фильтрацию по датам, статусам
- подсчёт общего количества записей через отдельный
COUNT(*)запрос - защиту от слишком больших значений
limit(например, максимум 100) - кэширование первых страниц
Для cursor-based подхода в Go часто используют base64-кодирование курсора, чтобы скрыть внутренний формат от клиента. Например:
GOcursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d:%s", id, timestamp)))
В REST API ответ обычно выглядит так:
JSON{"data": [...],"pagination": {"total": 100,"page": 2,"per_page": 20,"has_more": true}}
Для cursor-based:
JSON{"data": [...],"next_cursor": "abc123","has_more": true}
Пример кода
Пример offset-based пагинации в Go с использованием net/http и GORM:
GOtype PaginationParams struct {Page intPerPage int}func parsePagination(r *http.Request) (PaginationParams, error) {page, err := strconv.Atoi(r.URL.Query().Get("page"))if err != nil || page < 1 {page = 1}perPage, err := strconv.Atoi(r.URL.Query().Get("per_page"))if err != nil || perPage < 1 {perPage = 20}if perPage > 100 {perPage = 100}return PaginationParams{Page: page, PerPage: perPage}, nil}func ListUsersHandler(db *gorm.DB) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {params, _ := parsePagination(r)offset := (params.Page - 1) * params.PerPagevar users []Uservar total int64db.Model(&User{}).Count(&total)db.Order("id ASC").Limit(params.PerPage).Offset(offset).Find(&users)response := map[string]interface{}{"data": users,"pagination": map[string]interface{}{"total": total,"page": params.Page,"per_page": params.PerPage,"has_more": offset+len(users) < int(total),},}json.NewEncoder(w).Encode(response)}}
Пример cursor-based пагинации:
GOfunc ListMessagesHandler(db *gorm.DB) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {cursor := r.URL.Query().Get("cursor")limit := 50query := db.Model(&Message{}).Order("id DESC").Limit(limit)if cursor != "" {decoded, _ := base64.StdEncoding.DecodeString(cursor)cursorID, _ := strconv.Atoi(string(decoded))query = query.Where("id < ?", cursorID)}var messages []Messagequery.Find(&messages)var nextCursor stringhasMore := len(messages) == limitif hasMore {lastID := messages[len(messages)-1].IDnextCursor = base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(lastID)))}json.NewEncoder(w).Encode(map[string]interface{}{"data": messages,"next_cursor": nextCursor,"has_more": hasMore,})}}
Как отвечать на собеседовании
Начните с разграничения двух подходов: offset-based и cursor-based. Объясните, когда какой выбирать. Упомяните, что в Go нет встроенных средств пагинации - это всегда ручная работа с параметрами запроса и ORM/драйвером БД. Покажите понимание trade-off: offset прост, но медленный на больших данных; cursor быстрый, но не поддерживает произвольный доступ к страницам.
Расскажите про валидацию входных параметров, дефолтные значения, ограничение максимального limit. Упомяните, что в production-проектах часто добавляют подсчёт total через отдельный запрос, но на больших таблицах это может быть дорого - тогда используют приблизительные оценки или отказываются от total.
Если спросят про GraphQL - упомяните Relay-style connection с first/after, это тот же cursor-based подход. Если спросят про gRPC - обычно используют page token, аналогичный cursor.
Что проверяет интервьюер
Интервьюер оценивает:
- понимание разницы между offset и cursor пагинацией
- знание особенностей работы БД с
OFFSETна больших объёмах - умение валидировать входные данные и устанавливать разумные ограничения
- способность спроектировать ответ API с метаданными
- понимание, когда пагинация не нужна (например, для маленьких справочников)
- знание конкретных инструментов: GORM, sqlx, database/sql
Для senior-позиции важно показать, что вы думаете о производительности, консистентности данных (например, при вставке новых записей во время пагинации) и удобстве клиентского API.
Типичные ошибки
- не валидировать параметры - отрицательный
limitилиoffsetвызывает ошибку БД - не ограничивать максимальный
limit- клиент может запросить миллион записей - использовать
OFFSETдля больших таблиц без осознания проблемы производительности - забывать про
COUNT(*)запрос - без него клиент не знает общее количество страниц - возвращать
next_cursorкак сырой ID - лучше кодировать в base64 - не учитывать сортировку при cursor-based пагинации - курсор должен быть связан с порядком сортировки
- игнорировать гонки: при offset-based пагинации новые записи сдвигают страницы, при cursor-based - нет
- парсить query параметры без обработки ошибок -
strconv.Atoiвернёт ошибку на нечисловом значении
> Похожие задачи по backend
Напиши функцию на Go, которая принимает слайс каналов int и возвращает канал с суммами чисел из каждого канала
Как реализовать эффективную пагинацию с курсорами и индексами
Как бороться с большими объемами данных в запросах, например шардирование
Почему запрос с индексами может тормозить и как это решать
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью