This series was originally written for my mailing list. Want to get some awesome Go tutorials like this delivered to your inbox every week or two? You can sign up for my mailing list at signup.calhoun.io or via the form at the bottom of this page 👇
Before I get into this exercise, I want everyone to be aware that this exercise assumes you have some basic Go experience. I don’t go into a ton of details about how HTTP servers work, routing, or anything like that. If you have gone throuhg my free course Gophercises, then you will probably find the pace to be a good fit. If you haven’t, you might want to get some familiarity with Go and the net/http package. One way to do this is to check out the samples (they are free) from my course Web Development with Go. These will cover all the basics of setting up a web server and routing, getting you into a good position to start this exercise.
The first thing I like to do with any project is to break it down into some manageable steps. Since we are going to be building a blog, I think the first few logical steps are:
I also know I want to eventually read the markdown files from another source, so I might start looking at ways to use an interface to make that easier when the time comes.
If you have seen the samples from Web Development with Go, the server part should be pretty straightforward. I created my directory, initialized my module, then I created a main.go source file.
mkdir jonblog
cd jonblog
go mod init github.com/joncalhoun/jonblog
code main.go
Inside the main source file I created a main function and setup a web server. I opted to use the new ServeMux changes from Go 1.22 in my routes, so I was able to use a named variable inside the path and limit it to only GET requests.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /posts/{slug}", func(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
fmt.Fprintf(w, "Post: %s", slug)
})
err := http.ListenAndServe(":3030", mux)
if err != nil {
log.Fatal(err)
}
}
Now I have a web application that will handle requests to pages like /posts/demo
or /posts/how-to-boil-eggs
, and it is parsing the slug from the URL. With this I can start reading the markdown file based on the slug.
What is a slug?
I don’t recall where the term came from, but in this context a slug is basically a user-facing unique ID for each blog post. We don’t call it an ID because it can technically change, and if we stored our blog posts in a database this would likely be a separate column than the primary ID due to its ability to be changed.
For blog posts a slug
is often used instead of a database ID because slugs tend to be easier for someone to discern what the post is about when glancing at a URL than a numeric ID or UUID.
Short term I am going to read the markdown from files stored locally on my computer, but long term I want the flexibility to read from somewhere else like GitHub. Knowing this, I’m going to do something I’d usually not encourage a beginner to do - I am going to use an interface right away.
type SlugReader interface {
Read(slug string) (string, error)
}
The main reason I tend to discourage this as an immediate first step is because I’ve seen countless examples of developers trying to extract things into interfaces before really understanding what the interface should be or if one is even useful. For instance, someone might use an interface for their SQL database so that they can switch to MongoDB down the road, only to later realize all those interfaces don’t really work well for both of those databases because they require a different way of thinking about how you interact with and store data.
In this particular case I am willing to take that risk because I know I’m going to try to read from another source later, and I know I need to be able to read a blog post’s markdown with nothing but a unique slug
to look it up. If I end up needing to tweak the interface a bit, it should be pretty minor and painless to do.
With the interface defined, I am going to create an implementation that reads markdown files from the local disk using the slug as the file name.
type FileReader struct{}
func (fsr FileReader) Read(slug string) (string, error) {
f, err := os.Open(slug + ".md")
if err != nil {
return "", err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}
With this I am ready to create a function that accepts a SlugReader
as an argument and then returns an HTTP handler function to render the post.
func PostHandler(sl SlugReader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
postMarkdown, err := sl.Read(slug)
if err != nil {
// TODO: Handle different errors in the future
http.Error(w, "Post not found", http.StatusNotFound)
return
}
fmt.Fprint(w, postMarkdown)
}
}
Finally, I’ll update the ServeMux code to use this new handler.
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /posts/{slug}", PostHandler(FileReader{}))
err := http.ListenAndServe(":3030", mux)
if err != nil {
log.Fatal(err)
}
}
Now if I create a file named “how-to-boil-eggs.md” and then I visit the page /posts/how-to-boil-eggs I will see the contents of that file being rendered. I’ll also be able to easily replace the FileReader
down the road with a GitHubReader
or whatever other implementation I want to use. I could even implement a stack that uses something else on a cache miss. We will see exactly how useful that is a little later on.
I think this is a good place to take a break. In the next post we will start looking at rendering markdown and using templates to add some of our own contents. If you want to check out the source code up until this point you can find it on Github under the p1 tag.
This article is part of the series, Exercise: Building a Blog in Go.
If you are feeling a bit lost, I recommend checking out a sample from my course, Web Development with Go. The sample includes 19 lessons that will help get you familiar enough with Go's net/http package to start working on this exercise and feeling a lot more comfortable with things like HTTP handler functions, ServeMux, and more.
You will also receive notifications when I release new articles, updates on upcoming courses (including FREE ones), and I'll let you know when my paid courses are on sale.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
More in this series
This post is part of the series, Exercise: Building a Blog in Go.
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.