go
57 lines · 8 steps
Cursor pagination in a Gin handler
A Gin endpoint that pages through posts using opaque, base64-encoded cursors instead of numeric offsets.
Explained by
highlit
1package handlers
2
3import (
4 "encoding/base64"
5 "net/http"
6 "strconv"
7
8 "github.com/gin-gonic/gin"
9)
10
11type PostList struct {
12 Data []Post `json:"data"`
13 NextPage *string `json:"next_page,omitempty"`
14}
15
16func encodeCursor(id int64) string {
17 return base64.URLEncoding.EncodeToString([]byte(strconv.FormatInt(id, 10)))
18}
19
20func decodeCursor(token string) (int64, error) {
21 raw, err := base64.URLEncoding.DecodeString(token)
22 if err != nil {
23 return 0, err
24 }
25 return strconv.ParseInt(string(raw), 10, 64)
26}
27
28func (h *Handler) ListPosts(c *gin.Context) {
29 limit, err := strconv.Atoi(c.DefaultQuery("limit", "20"))
30 if err != nil || limit < 1 || limit > 100 {
31 limit = 20
32 }
33
34 var afterID int64
35 if cursor := c.Query("cursor"); cursor != "" {
36 afterID, err = decodeCursor(cursor)
37 if err != nil {
38 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid cursor"})
39 return
40 }
41 }
42
43 posts, err := h.repo.ListAfter(c.Request.Context(), afterID, limit+1)
44 if err != nil {
45 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load posts"})
46 return
47 }
48
49 resp := PostList{Data: posts}
50 if len(posts) > limit {
51 resp.Data = posts[:limit]
52 next := encodeCursor(resp.Data[len(resp.Data)-1].ID)
53 resp.NextPage = &next
54 }
55
56 c.JSON(http.StatusOK, resp)
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Cursor pagination encodes the last item's key into an opaque token, avoiding the drift and cost of offset-based paging.
- 2Fetching limit+1 rows is a cheap trick to detect whether another page exists without a second query.
- 3A pointer field with omitempty lets you omit next_page entirely on the last page rather than sending null.
Related explainers
go
package hashring import ( "hash/crc32"
How consistent hashing works in Go
consistent-hashing
load-balancing
concurrency
Intermediate
8 steps
go
package middleware import ( "net/http"
Wrapping Gin requests in a DB transaction
middleware
transactions
error-handling
Intermediate
8 steps
go
package metrics import ( "sync"
A thread-safe sliding-window average in Go
concurrency
sliding-window
running-average
Intermediate
8 steps
ruby
require "charlock_holmes" class TextFileNormalizer DEFAULT_CONFIDENCE = 60
Normalizing text files to clean UTF-8 in Ruby
encoding
text-processing
file-io
Intermediate
8 steps
go
package logging import ( "fmt"
Redacting secrets with Go reflection
reflection
recursion
struct-tags
Advanced
10 steps
javascript
const express = require('express'); const router = express.Router(); const db = require('../db');
Building a paginated orders page in Express
pagination
routing
sql-queries
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/cursor-pagination-in-a-gin-handler-explained-go-6764/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.