Building a Blog with Go — Part 1
Updated 23 Sep 2024
Updated 23 Sep 2024
Over the past year or so, I've been using Obsidian to keep track of random thoughts and rants that would normally stay stuck in my head. I love Obsidian because it's just Markdown files on my local machine. Recently, I was setting up a new VPS and I thought, "Hell, I've got all these markdown files, I've got unlimited subdomains, why not set up a blog?"
Lucky for me, Deno had a blog module. You just tweak one JS config file and... tada, you've got a blog. It had a nice enough looking UI out of the box and it was possible to self-host. I created a deployment pipeline such that I could create or update markdown files in my Obsidian vault on my local machine and then just run one command to build and deploy the updated version of the blog to my VPS via a webhook. I even had my markdown directory set up as a git submodule. However at some point after the release of Deno 1.44.3, my blog stopped building and I took that as an excuse to rewrite it in Go. I liked the way my Deno blog looked so I decided to recreate it with Go's templating package html/template
. I’ve done a little bit of Wordpress development and always kind of enjoyed using html templating. Plus I love writing CSS.
Here's my project's Github repo for reference. I decided to stick pretty closely with Go's recommended project structure for building a server:
├── cmd
└── web
├── handlers.go
├── helpers.go
├── main.go
└── routes.go
├── internal
├── models
└── validator
├── sql
└── schema
└── ui
├── html
├── static
└── efs.go
├── devblog.db
├── go.mod
└── go.sum
The cmd
directory contains logic specific to the web
application and is where we'll be hanging out today. The cmd
directory will also eventually include a separate cli
directory for a command-line tool I'm planning on adding. The internal
directory holds all the non-application specific logic like interacting with the database and helper functions for HTML form validation. Schema files are in the sql
directory and ui
contains both HTML templates and other static assets like CSS and images.
The efs.go
file is actually really cool as it allows to me to embed my entire ui
directory into the actual binary. This embedded file system means that the compiled binary executable will contain everything my application needs to run.
In case it isn’t obvious, devblog.db
is a SQLite database file, go.mod
is the project manifest that defines dependencies, versions, and module paths, and go.sum
records the checksums of said dependencies.
Let's take a quick look at main.go
before we go dive into a couple other files in cmd
. Side note: I've linked to the file above since it can be kind of annoying to read in the format below.
type application struct {
logger *slog.Logger
posts *models.PostModel
users *models.UserModel
formDecoder *form.Decoder
templateCache map[string]*template.Template
sessionManager *scs.SessionManager
}
func main() {
addr := flag.String("addr", ":4000", "HTTP network address")
dsn := flag.String("dsn", "", "Path to SQLite db")
flag.Parse()
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
}))
db, err := openDB(*dsn)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
defer db.Close()
templateCache, err := newTemplateCache()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
formDecoder := form.NewDecoder()
sessionManager := scs.New()
sessionManager.Store = sqlite3store.New(db)
sessionManager.Lifetime = 12 * time.Hour
app := &application{
logger: logger,
posts: &models.PostModel{DB: db},
users: &models.UserModel{DB: db},
templateCache: templateCache,
formDecoder: formDecoder,
sessionManager: sessionManager,
}
srv := &http.Server{
Addr: *addr,
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
}
logger.Info("starting server", "addr", srv.Addr)
err = srv.ListenAndServe()
logger.Error(err.Error())
os.Exit(1)
}
The first lines of code are defining a custom application
object that must contain certain fields. At the top of the main()
function, the port number and database connection string are passed in via command line flags. This allows me to define default values and provide --help
functionality for my environment variables. Plus I can still use a .env
file at the project root and just source it in my shell session:
source ./env
go run ./cmd/web -dsn=$DSN
I then create a centralized logger, a database connection pool, an html template cache, a form decoder, and a session manager and add them to a new instance of my custom application
struct. This allows me to define my handler functions as methods against my application struct so I always have access to its fields. This pattern of dependency injection works well because all my handler functions will be defined in my main
package. Lastly, I initialize an http.Server
struct with the centralized logger, the port, the router, and some custom timeout settings.
A couple of syntax things to note:
&application{}
syntax is a way to create a new instance of the application struct and get a pointer to it.:=
syntax is a shorthand for initializing a variable and assigning it a value. So foo := "bar"
would initialize the variable foo
and assign it the value of the string "bar".if err != nil {}
is Go's syntax for error handling. Take the following code:db, err := openDB(*dsn)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
Here, the openDB
function returns two values — a pointer to a pool of database connections which we assign to the variable db
and an error which we assign to err
. If our call to openDB
returns a non-nil value for err
, we need to specify what our program should do. In this case, we log the error and exit the program with the status code 1. A lot of people hate the way Go treats errors as values but personally, I love it.
Most Go applications will only have one servemux (router) where you define how different URL paths are handled. In the routes()
function below, we're initializing that servemux on the first line. We then repeatedly invoke its Handle
method to register handler functions for specific URL patterns.
Static files are served under the /static/
path, and specific handlers are set up for rendering pages like the home page, viewing posts, user login, creating posts, and updating posts. Paths with a trailing "/" are known as subtree path patterns and will act as if they have a wildcard at the end. Thus in order to make the "home" route match "/" and nothing else, I restricted the path by adding "{$}" to the end of it.
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
mux.Handle("GET /static/", http.FileServerFS(ui.Files))
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
mux.Handle("GET /{$}", dynamic.ThenFunc(app.home))
mux.Handle("GET /posts/{id}", dynamic.ThenFunc(app.handlerPostView))
mux.Handle("GET /user/login", dynamic.ThenFunc(app.handlerUserLogin))
mux.Handle("POST /user/login", dynamic.ThenFunc(app.handlerUserLoginPost))
protected := dynamic.Append(app.requireAuthentication)
mux.Handle("GET /post/create", protected.ThenFunc(app.handlerPostCreate))
mux.Handle("GET /post/update/{id}", protected.ThenFunc(app.handlerPostUpdate))
mux.Handle("POST /post/create", protected.ThenFunc(app.handlerPostCreatePost))
mux.Handle("/post/update/{id}", protected.ThenFunc(app.handlerPostUpdatePut))
mux.Handle("POST /user/logout", protected.ThenFunc(app.handlerUserLogoutPost))
standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders)
return standard.Then(mux)
}
You'll notice that every handler function is wrapped by a chain of middleware functions that run before the handler function. The dynamic
middleware chain includes LoadAndSave
for session management, noSurf
for CSRF protection, and app.authenticate
for user authentication. The protected
middleware chain extends dynamic
by adding app.requireAuthentication
to enforce user login for certain routes. The standard
middleware chain applies global middleware to all routes for error recovery, request logging, and adding common headers. Session management is provided by the github.com/alexedwards/scs package. The CSRF protection and middleware chaining is provided by the github.com/justinas/nosurf and github.com/justinas/alice packages respectively.
Let's take a look at the code for the app.home()
function:
func (app *application) home(w http.ResponseWriter, r *http.Request) {
posts, err := app.posts.Latest()
if err != nil {
app.serverError(w, r, err)
return
}
tag := r.URL.Query().Get("tag")
filteredPosts := []models.Post{}
if tag != "" {
for _, p := range posts {
for _, t := range p.Tags {
if t == tag {
filteredPosts = append(filteredPosts, p)
}
}
}
}
data := app.newTemplateData(r)
data.Posts = posts
if len(filteredPosts) > 0 {
data.Posts = filteredPosts
}
app.render(w, r, http.StatusOK, "home.tmpl.html", data)
}
First let's talk about the function signature. In Go, you can define methods on any type. Here, I'm defining the home()
function as a method against the application struct by including the special receiver argument (app *application)
. That's all you need to do to define a method. Looking at the home()
function's parameters, we can see it takes an http.ResponseWriter
and a pointer to an http.Request
as its arguments. This means that it satisfies the http.HandlerFunc
interface which means it can be used as an http.Handler
. Go doesn't require you to explicitly state that a type implements an interface in Go. If the function signatures match, Go will recognize the implementation. Now in our case, the home()
function doesn't return anything but it could return multiple values as I mentioned before when discussing error handling.
The body of the function is pretty straightforward. We grab the most recent posts from the db and assign them to the posts
variable. Then we grab the "tag" value from the URL's query string (if it exists) and filter out any posts that don't contain the tag. Then we initialize a data
struct, assign our posts
to its Posts
field, and finally render our HTML with the data. We'll get more into the newTemplateData
and render
methods in part 2.
I should note that the code for filtering posts based on the tag value is not ideal but I'm leaving it as is for a number of reasons. For context, when I initially set up the database model, I decided to give tags their own table. This normalization made for a slightly more complicated setup but I didn't mind because this whole blog is over-engineered and that's kind of the point. When it came time to add the functionality for filtering the home page based on a specific tag, the most straightforward solution was a nested for loop.
As someone who primarily writes Javascript and Python, this solution just felt wrong. Surely there's some better way of doing this that doesn't rely on for loops. But in Go, if you need to loop over something, you just use a loop. I find that so refreshing. Sure, I could rewrite it to avoid O(n^2) but who cares. Go is super performant and in this case, it will never be a problem. And it took me all of five minutes to write the solution.
That's all for this post. Part 2 will go deeper on the the front end of the blog as well as some of the database logic. Thanks for reading!