You're reading a sample of this book. Get the full version here.
Let's Go Foundations › Routing requests
Previous · Contents · Next
Chapter 2.3.

Routing requests

Having a web application with just one route isn’t very exciting… or useful! Let’s add a couple more routes so that the application starts to shape up like this:

Route pattern Handler Action
/ home Display the home page
/snippet/view snippetView Display a specific snippet
/snippet/create snippetCreate Display a form for creating a new snippet

Reopen the main.go file and update it as follows:

File: main.go
package main

import (
    "log"
    "net/http"
)

func home(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello from Snippetbox"))
}

// Add a snippetView handler function.
func snippetView(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Display a specific snippet..."))
}

// Add a snippetCreate handler function.
func snippetCreate(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Display a form for creating a new snippet..."))
}

func main() {
    // Register the two new handler functions and corresponding route patterns with
    // the servemux, in exactly the same way that we did before.
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)
    mux.HandleFunc("/snippet/view", snippetView)
    mux.HandleFunc("/snippet/create", snippetCreate)

    log.Print("starting server on :4000")

    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

Make sure these changes are saved and then restart the web application:

$ cd $HOME/code/snippetbox
$ go run .
2024/03/18 11:29:23 starting server on :4000

If you visit the following links in your web browser you should now get the appropriate response for each route:

02.03-01.png
02.03-02.png

Trailing slashes in route patterns

Now that the two new routes are up and running, let’s talk a bit of theory.

It’s important to know that Go’s servemux has different matching rules depending on whether a route pattern ends with a trailing slash or not.

Our two new route patterns — "/snippet/view" and "/snippet/create" — don’t end in a trailing slash. When a pattern doesn’t have a trailing slash, it will only be matched (and the corresponding handler called) when the request URL path exactly matches the pattern in full.

When a route pattern ends with a trailing slash — like "/" or "/static/" — it is known as a subtree path pattern. Subtree path patterns are matched (and the corresponding handler called) whenever the start of a request URL path matches the subtree path. If it helps your understanding, you can think of subtree paths as acting a bit like they have a wildcard at the end, like "/**" or "/static/**".

This helps explain why the "/" route pattern acts like a catch-all. The pattern essentially means match a single slash, followed by anything (or nothing at all).

Restricting subtree paths

To prevent subtree path patterns from acting like they have a wildcard at the end, you can append the special character sequence {$} to the end of the pattern — like "/{$}" or "/static/{$}".

So if you have the route pattern "/{$}", it effectively means match a single slash, followed by nothing else. It will only match requests where the URL path is exactly /.

Let’s use this in our application to stop our home handler acting as a catch all, like so:

File: main.go
package main

...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/{$}", home) // Restrict this route to exact matches on / only.
    mux.HandleFunc("/snippet/view", snippetView) 
    mux.HandleFunc("/snippet/create", snippetCreate)

    log.Print("starting server on :4000")

    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

Once you’ve made that change, restart the server and make a request for an unregistered URL path like http://localhost:4000/foo/bar. You should now get a 404 response which looks a bit like this:

02.03-03.png

Additional information

Additional servemux features

There are a couple of other servemux features worth pointing out:

Host name matching

It’s possible to include host names in your route patterns. This can be useful when you want to redirect all HTTP requests to a canonical URL, or if your application is acting as the back end for multiple sites or services. For example:

mux := http.NewServeMux()
mux.HandleFunc("foo.example.org/", fooHandler)
mux.HandleFunc("bar.example.org/", barHandler)
mux.HandleFunc("/baz", bazHandler)

When it comes to pattern matching, any host-specific patterns will be checked first and if there is a match the request will be dispatched to the corresponding handler. Only when there isn’t a host-specific match found will the non-host specific patterns also be checked.

The default servemux

If you’ve been working with Go for a while you might have come across the http.Handle() and http.HandleFunc() functions. These allow you to register routes without explicitly declaring a servemux, like this:

func main() {
    http.HandleFunc("/", home)
    http.HandleFunc("/snippet/view", snippetView)
    http.HandleFunc("/snippet/create", snippetCreate)

    log.Print("starting server on :4000")
    
    err := http.ListenAndServe(":4000", nil)
    log.Fatal(err)
}

Behind the scenes, these functions register their routes with something called the default servemux. This is just a regular servemux like we’ve already been using, but which is initialized automatically by Go and stored in the http.DefaultServeMux global variable.

If you pass nil as the second argument to http.ListenAndServe(), the server will use http.DefaultServeMux for routing.

Although this approach can make your code slightly shorter, I don’t recommend it for two reasons:

So, for the sake of clarity, maintainability and security, it’s generally a good idea to avoid http.DefaultServeMux and the corresponding helper functions. Use your own locally-scoped servemux instead, like we have been doing in this project so far.