How to do Pagination with Gin
- 6 minutes read - 1160 wordsThis is the ninth in a series of articles about writing a small reading list app in Go for personal use.
Last week we wrote a tool to import books from a CSV file exported from a service like Goodreads to our book database. And we found that it takes about ten times longer to render the book listing page when there are 600 books than when there are three books.
To keep page generation times shorter, we’ll use a strategy called “pagination” to split the book listing up into multiple pages, with each page having a limited number of books on it.
When we are done with this article, we’ll have:
- the books list paginated so that it only shows 15 books at a time
- with multiple pages to show all the books
- and page-by-page navigation links at the bottom of the page

A Page from ‘The Canterbury Tales,’ by Geoffrey Chaucer, printed at Westminster by William Caxton, 1477.
The Strategy
Our strategy is fairly simple: we will add a query parameter page
to the
book index page.
In the book index handler we will query the number of books in the table and use that to determine the number of pages that are in the index.
We will check that the page is valid, returning an error code if it is not.
Then we will determine the offset from the beginning of the books table, and query the table from that offset, limiting our result set to the page size we’ve determined.
Finally, we’ll pass the list of books to the template as before, but now we’ll also add some page data:
- the current page
- the total number of pages
- the previous page (empty if the first page)
- the next page (empty if the last page)
- a slice of ints so that the template has all of the page numbers
The Code
We are going to mostly rewrite bookIndexGet
so I will show the whole
function, chunk by chunk.
First we use c.DefaultQuery
to get the page
query parameter, defaulting
to the string "1"
if the parameter is not present. Then we convert it to
an integer, aborting with a 400 error if the conversion fails.
func bookIndexGet(c *gin.Context) {
db := c.Value("database").(*gorm.DB)
pageStr := c.DefaultQuery("page", "1")
page, err := strconv.Atoi(pageStr)
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
In the block below we get the count of books in the table, then divide by
booksPerPage
to get the number of pages. We have to make sure there’s at
least one page even in the case that the table is empty. Then we make sure
the page we were given in the query parameter is valid, aborting with a 400
error if it is not.
var bookCount int64
if err := db.Table("books").Count(&bookCount).Error; err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
const booksPerPage = 15
pageCount := int(math.Ceil(float64(bookCount) / float64(booksPerPage)))
if pageCount == 0 {
pageCount = 1
}
if page < 1 || page > pageCount {
c.AbortWithStatus(http.StatusBadRequest)
return
}
Now we’re ready to query the books from the database. We calculate the offset and perform the query to load books into our slice.
offset := (page - 1) * booksPerPage
books := []Book{}
if err := db.Limit(booksPerPage).Offset(offset).Find(&books).Error; err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
Finally, we can call the template and return the page to the user. To do this, we have to calculate the previous page number (unless this is the first page), and the next page number (unless this is the last page). We also fill a slice of int with all of the page numbers. We do this because there’s no built-in mechanism in Go’s templates to count from 1 to N – only support for ranging over slices. (We could also inject a function to be called from the template, but that’s a topic for a future article.)
var prevPage, nextPage string
if page > 1 {
prevPage = fmt.Sprintf("%d", page-1)
}
if page < pageCount {
nextPage = fmt.Sprintf("%d", page+1)
}
pages := make([]int, pageCount)
for i := 0; i < pageCount; i++ {
pages[i] = i + 1
}
c.HTML(http.StatusOK, "books/index.html", gin.H{
"books": books,
"pageCount": pageCount,
"page": page,
"prevPage": prevPage,
"nextPage": nextPage,
"pages": pages,
})
} // end of bookIndexGet
Here’s the full modified template templates/books/index.html
:
{{ define "books/index.html" }}{{ template "base/header.html" . }}
<h1>My Books</h1>
<div>
Page {{ .page }}/{{ .pageCount }}
</div>
<ul class="books">
{{ range .books }}
<li class="book">
<span class="title">{{ .Title }}</span>
<span class="author">{{ .Author }}</span>
</li>
{{ end }}
</ul>
<div>
Pages:
{{ if .prevPage }}
<a href="/books/?page={{ .prevPage }}">«</a>
{{ end }}
{{ range .pages }}
{{ if eq . $.page }}<strong>{{ . }}</strong>
{{ else }}<a href="/books/?page={{ . }}">{{ . }}</a>
{{ end }}
{{ end }}
{{ if .nextPage }}
<a href="/books/?page={{ .nextPage }}">»</a>
{{ end }}
</div>
{{ template "base/footer.html" . }}
{{ end }}
At the top we’ve added a div
that shows the current page and the total
number of pages. At the bottom we provide a set of links that go to each
page. To the left of those links we provide a «
link to the previous
page unless we’re already at the front. And to the right we provide a »
link unless we’re already at the back.
This collection of links is something we could consider limiting: if the application might have thousands of books and hundreds of pages (or more) then it will be cumbersome for the user to deal with all those links at the bottom and also expensive for us to generate. In this case we might limit the number of links shown to some small number before and after the current page. (And we definitely need to provide some alternative way for them to find books – like searching – instead of paging through a list that is in an arbitrary order. But again, this is a topic for a future article.)

A screenshot of the book list with pagination.
In terms of time, we’ve gone from the 10ms we were seeing in last week’s article to 1.1ms… which is pretty close to the 10x improvement I talked about last week. (Confession: 10x was a hopeful guess. I’m mildly surprised it was that close.)
[GIN] 2022/05/13 - 23:17:09 | 200 | 1.149047ms | 127.0.0.1| GET "/books/?page=3"
Next Week
Alert readers will notice that I have neglected to include any tests this week. No fear! They have been left as both an exercise for the reader and something to look forward to next week with a new and exiciting twist as we explore Go 1.18’s new support for fuzz testing against our pagination code.