This 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
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
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.
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.
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.)
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.)
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.)
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.