You're reading a sample of this book. Get the full version here.
Let's Go Foundations › Wildcard route patterns
Previous · Contents · Next
Chapter 2.4.

Wildcard route patterns

It’s also possible to define route patterns that contain wildcard segments. You can use these to create more flexible routing rules, and also to pass variables to your Go application via a request URL. If you’ve built web applications using frameworks in other languages before, the concepts in this chapter will probably feel familiar to you.

Let’s step away from our application build for a moment to explain how it works.

Wildcard segments in a route pattern are denoted by an wildcard identifier inside {} brackets. Like this:

mux.HandleFunc("/products/{category}/item/{itemID}", exampleHandler)

In this example, the route pattern contains two wildcard segments. The first segment has the identifier category and the second has the identifier itemID.

The matching rules for route patterns containing wildcard segments are the same as we saw in the previous chapter, with the additional rule that the request path can contain any non-empty value for the wildcard segments. So, for example, the following requests would all match the route we defined above:

/products/hammocks/item/sku123456789
/products/seasonal-plants/item/pdt-1234-wxyz
/products/experimental_foods/item/quantum%20bananas

Inside your handler, you can retrieve the corresponding value for a wildcard segment using its identifier and the r.PathValue() method. For example:

func exampleHandler(w http.ResponseWriter, r *http.Request) {
    category := r.PathValue("category")
    itemID := r.PathValue("itemID")

    ...
}

The r.PathValue() method always returns a string value, and it’s important to remember that this can be any value that the user includes in the URL — so you should validate or sanity check the value before doing anything important with it.

Using wildcard segments in practice

OK, let’s get back to our application and update it to include a new {id} wildcard segment in the /snippet/view route, so that our routes look like this:

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

Open up your main.go file, and make this change like so:

File: main.go
package main

...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/{$}", home)
    mux.HandleFunc("/snippet/view/{id}", snippetView)  // Add the {id} wildcard segment
    mux.HandleFunc("/snippet/create", snippetCreate)

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

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

Now let’s head to our snippetView handler and update it to retrieve the id value from the request URL path. Later in the book, we’ll use this id value to select a specific snippet from a database, but for now we’ll just echo the id back to the user as part of the HTTP response.

Because the id value is untrusted user input, we should validate it to make sure it’s sane and sensible before we use it. For the purpose of our application we want to check that the id value contains a positive integer, which we can do by trying to convert the string value to an integer with the strconv.Atoi() function and then checking that the value is greater than zero.

Here’s how:

File: main.go
package main

import (
    "fmt" // New import
    "log"
    "net/http"
    "strconv" // New import
)

...

func snippetView(w http.ResponseWriter, r *http.Request) {
    // Extract the value of the id wildcard from the request using r.PathValue()
    // and try to convert it to an integer using the strconv.Atoi() function. If
    // it can't be converted to an integer, or the value is less than 1, we
    // return a 404 page not found response.
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    // Use the fmt.Sprintf() function to interpolate the id value with a
    // message, then write it as the HTTP response.
    msg := fmt.Sprintf("Display a specific snippet with ID %d...", id)
    w.Write([]byte(msg))
}

...

Save your changes, restart the application, then open your browser and try visiting a URL like http://localhost:4000/snippet/view/123. You should see a response containing the echoed id wildcard value from the request URL, similar to this:

02.04-01.png

You might also like to try visiting some URLs which have invalid values for the id wildcard, or no wildcard value at all. For instance:

For all these requests you should get a 404 page not found response.


Additional information

Precedence and conflicts

When defining route patterns with wildcard segments, it’s possible that some of your patterns will ‘overlap’. For example, if you define routes with the patterns "/post/edit" and "/post/{id}" they overlap because an incoming HTTP request with the path /post/edit is a valid match for both patterns.

When route patterns overlap, Go’s servemux needs to decide which pattern takes precedent so it can dispatch the request to the appropriate handler.

The rule for this is very neat and succinct: the most specific route pattern wins. Formally, Go defines a pattern as more specific than another if it matches only a subset of requests that the other pattern matches.

Continuing with the example above, the route pattern "/post/edit" only matches requests with the exact path /post/edit, whereas the pattern "/post/{id}" matches requests with the path /post/edit, /post/123, /post/abc and many more. Therefore "/post/edit" is the more specific route pattern and will take precedent.

While we’re on this topic, there are a few other things worth mentioning:

Subtree path patterns with wildcards

It’s important to understand that the routing rules we described in the previous chapter still apply, even when you’re using wildcard segments. In particular, if your route pattern ends in a trailing slash and has no {$} at the end, then it is treated as a subtree path pattern and it only requires the start of a request URL path to match.

So, if you have a subtree path pattern like "/user/{id}/" in your routes (note the trailing slash), this pattern will match requests like /user/1/, /user/2/a, /user/2/a/b/c and so on.

Again, if you don’t want that behavior, stick a {$} at the end — like "/user/{id}/{$}".

Remainder wildcards

Wildcards in route patterns normally match a single, non-empty, segment of the request path only. But there is one special case.

If a route pattern ends with a wildcard, and this final wildcard identifier ends in ..., then the wildcard will match any and all remaining segments of a request path.

For example, if you declare a route pattern like "/post/{path...}" it will match requests like /post/a, /post/a/b, /post/a/b/c and so on — very much like a subtree path pattern does. But the difference is that you can access the entire wildcard part via the r.PathValue() method in your handlers. In this example, you could get the wildcard value for {path...} by calling r.PathValue("path").